diff --git a/.config-audit-ignore b/.config-audit-ignore index f79ad0f4..8c98043b 100644 --- a/.config-audit-ignore +++ b/.config-audit-ignore @@ -15,7 +15,9 @@ # Vite environment variables are accessed through import.meta.env and may not # be directly referenced in source code but are used by the build process -# VITE_STELLAR_NETWORK +# Read via `import.meta.env?.VITE_STELLAR_NETWORK` in +# dashboard/src/config/stellarNetwork.ts, which the usage scanner does not match. +VITE_STELLAR_NETWORK # VITE_EVENTS_API_URL # Node.js runtime environment variable diff --git a/_bracecheck.mjs b/_bracecheck.mjs new file mode 100644 index 00000000..26a5a4f3 --- /dev/null +++ b/_bracecheck.mjs @@ -0,0 +1,48 @@ +import fs from 'fs'; + +const src = fs.readFileSync(process.argv[2], 'utf8'); + +let i = 0; +let line = 1; +const stack = []; +let state = 'code'; // code | line | block | sq | dq | tpl + +function report(ch, depth) { + if ('{}()[]'.includes(ch)) { + if ('{(['.includes(ch)) { + stack.push({ ch, line }); + } else { + const open = stack.pop(); + const match = { '}': '{', ')': '(', ']': '[' }[ch]; + if (!open || open.ch !== match) { + console.log(`UNBALANCED at line ${line}: saw '${ch}', expected close for '${open ? open.ch : 'nothing'}' (opened line ${open ? open.line : '-'})`); + } + } + } +} + +for (i = 0; i < src.length; i++) { + const c = src[i]; + const n = src[i + 1]; + if (c === '\n') line++; + if (state === 'code') { + if (c === '/' && n === '/') { state = 'line'; i++; continue; } + if (c === '/' && n === '*') { state = 'block'; i++; continue; } + if (c === '"') { state = 'dq'; continue; } + if (c === "'") { state = 'sq'; continue; } + if (c === '`') { state = 'tpl'; continue; } + report(c); + } else if (state === 'line') { + if (c === '\n') state = 'code'; + } else if (state === 'block') { + if (c === '*' && n === '/') { state = 'code'; i++; } + } else if (state === 'dq') { + if (c === '\\') { i++; } else if (c === '"') state = 'code'; + } else if (state === 'sq') { + if (c === '\\') { i++; } else if (c === "'") state = 'code'; + } else if (state === 'tpl') { + if (c === '\\') { i++; } else if (c === '`') state = 'code'; + } +} + +console.log(`Remaining open at EOF: ${stack.map((s) => `${s.ch}@${s.line}`).join(', ') || 'none'}`); diff --git a/_bt.txt b/_bt.txt new file mode 100644 index 00000000..b18e1bcc --- /dev/null +++ b/_bt.txt @@ -0,0 +1,152 @@ +export interface ContractConfig { + address: string; + events: string[]; + /** Optional user ID for per-user notification preference gating */ + userId?: string; +} + +export interface DiscordConfig { + webhookUrl: string; + webhookId: string; + retryCount?: number; + backoffBaseSeconds?: number; + deduplicationWindowMs?: number; + deduplicationMaxSize?: number; + timeoutMs?: number; +} + +export interface RetryQueueConfig { + baseDelayMs?: number; + multiplier?: number; + jitter?: boolean; + maxRetries?: number; + processIntervalMs?: number; +} + +export interface WebhookSecret { + id: string; + secret: string; +} + +export interface RateLimitConfig { + enabled: boolean; + windowMs: number; + maxRequests: number; + clientOverrides: Record; +} + +export interface ApiKey { + key: string; + name?: string; +} + +export interface Config { + stellarNetwork: string; + stellarRpcUrl: string; + stellarNetworkPassphrase: string; + contractAddresses: ContractConfig[]; + pollIntervalMs: number; + maxReconnectAttempts: number; + reconnectDelayMs: number; + eventsApiPort: number; + eventsApiCorsOrigin: string; + discord?: DiscordConfig; + retryQueue?: RetryQueueConfig; + eventQueue?: EventQueueConfig; + webhookSecrets?: WebhookSecret[]; + apiKeys?: ApiKey[]; + scheduler?: SchedulerConfig; + retryScheduler?: RetrySchedulerOptions; + databasePath?: string; + rateLimit?: RateLimitConfig; + cleanup?: AppCleanupConfig; + analytics?: AnalyticsConfig; + backfill?: BackfillConfig; +} + +export interface SchedulerConfig { + enabled: boolean; + pollIntervalMs: number; + lockTimeoutMs: number; + processorId?: string; + batchSize: number; + timingBufferMs: number; +} + +export interface EventQueueConfig { + /** Maximum number of events to process concurrently (default: 1, must be >= 1). */ + maxConcurrency?: number; + /** Maximum retry attempts per event (default: 3). */ + maxRetries?: number; + /** Base delay in ms for exponential backoff (default: 2000). */ + baseDelayMs?: number; + /** How often to poll the queue for due events in ms (default: 1000). */ + pollIntervalMs?: number; +} + +export interface AppCleanupConfig { + /** How often to run cleanup jobs (ms). */ + intervalMs: number; + /** Retain completed/failed/cancelled notifications for this long (ms). */ + notificationRetentionMs: number; + /** Retain rate-limit audit rows for this long (ms). */ + rateLimitEventRetentionMs: number; + /** Retain in-memory events for this long (ms). */ + eventRetentionMs: number; + /** Retain notification execution log rows for this long (ms). */ + executionLogRetentionMs: number; +} + +export interface RetrySchedulerOptions { + enabled: boolean; + pollIntervalMs: number; + lockTimeoutMs: number; + processorId?: string; + batchSize: number; + baseDelayMs: number; + multiplier: number; + maxDelayMs: number; + jitter: boolean; +} + +export interface AnalyticsConfig { + enabled: boolean; + maxRecords: number; + maxBuckets: number; + bucketSizeMs: number; + /** How often to persist summarized snapshots (ms). */ + persistIntervalMs: number; + /** How long to retain persisted snapshots (days). */ + snapshotRetentionDays: number; +} + +export interface BackfillConfig { + /** + * When true, a backfill run is triggered once at startup before the live + * polling loop begins. + */ + enabled: boolean; + /** + * The earliest ledger from which to recover missed events (inclusive). + * Events at or after this ledger that are not yet in processed_events will + * be passed through the normal processing pipeline. + */ + startLedger: number; + /** + * The last ledger to include in the backfill window (inclusive). + * Defaults to the current network-tip ledger when not set. + */ + endLedger?: number; + /** + * Maximum number of Stellar RPC pages to fetch per contract during the + * backfill. Each page holds up to 200 events. Defaults to 50. + */ + maxPages?: number; + /** + * Upper bound on the total number of on-chain events processed per + * contract. Acts as a safety rail against unexpectedly large windows. + * Defaults to 10 000. + */ + maxEventsPerContract?: number; +} + diff --git a/_bt2.txt b/_bt2.txt new file mode 100644 index 00000000..0d3ae840 --- /dev/null +++ b/_bt2.txt @@ -0,0 +1,151 @@ +export interface ContractConfig { + address: string; + events: string[]; + /** Optional user ID for per-user notification preference gating */ + userId?: string; +} + +export interface DiscordConfig { + webhookUrl: string; + webhookId: string; + retryCount?: number; + backoffBaseSeconds?: number; + deduplicationWindowMs?: number; + deduplicationMaxSize?: number; + timeoutMs?: number; +} + +export interface RetryQueueConfig { + baseDelayMs?: number; + multiplier?: number; + jitter?: boolean; + maxRetries?: number; + processIntervalMs?: number; +} + +export interface WebhookSecret { + id: string; + secret: string; +} + +export interface RateLimitConfig { + enabled: boolean; + windowMs: number; + maxRequests: number; + clientOverrides: Record; +} + +export interface ApiKey { + key: string; + name?: string; +} + +export interface Config { + stellarNetwork: string; + stellarRpcUrl: string; + stellarNetworkPassphrase: string; + contractAddresses: ContractConfig[]; + pollIntervalMs: number; + maxReconnectAttempts: number; + reconnectDelayMs: number; + eventsApiPort: number; + eventsApiCorsOrigin: string; + discord?: DiscordConfig; + retryQueue?: RetryQueueConfig; + eventQueue?: EventQueueConfig; + webhookSecrets?: WebhookSecret[]; + apiKeys?: ApiKey[]; + scheduler?: SchedulerConfig; + retryScheduler?: RetrySchedulerOptions; + databasePath?: string; + rateLimit?: RateLimitConfig; + cleanup?: AppCleanupConfig; + analytics?: AnalyticsConfig; + expiration?: ExpirationConfig; + backfill?: BackfillConfig; +} + +export interface SchedulerConfig { + enabled: boolean; + pollIntervalMs: number; + lockTimeoutMs: number; + processorId?: string; + batchSize: number; + timingBufferMs: number; +} + +export interface EventQueueConfig { + /** Maximum number of events to process concurrently (default: 1, must be >= 1). */ + maxConcurrency?: number; + /** Maximum retry attempts per event (default: 3). */ + maxRetries?: number; + /** Base delay in ms for exponential backoff (default: 2000). */ + baseDelayMs?: number; + /** How often to poll the queue for due events in ms (default: 1000). */ + pollIntervalMs?: number; +} + +export interface AppCleanupConfig { + /** How often to run cleanup jobs (ms). */ + intervalMs: number; + /** Retain completed/failed/cancelled notifications for this long (ms). */ + notificationRetentionMs: number; + /** Retain rate-limit audit rows for this long (ms). */ + rateLimitEventRetentionMs: number; + /** Retain in-memory events for this long (ms). */ + eventRetentionMs: number; + /** Retain notification execution log rows for this long (ms). */ + executionLogRetentionMs: number; +} + +export interface RetrySchedulerOptions { + enabled: boolean; + pollIntervalMs: number; + lockTimeoutMs: number; + processorId?: string; + batchSize: number; + baseDelayMs: number; + multiplier: number; + maxDelayMs: number; + jitter: boolean; +} + +export interface AnalyticsConfig { + enabled: boolean; + maxRecords: number; + maxBuckets: number; + bucketSizeMs: number; + /** How often to persist summarized snapshots (ms). */ + persistIntervalMs: number; + /** How long to retain persisted snapshots (days). */ + snapshotRetentionDays: number; +} + +export interface ExpirationConfig { + /** Default expiration time in milliseconds (default: 24 hours = 86400000). */ + defaultExpirationMs: number; + /** Per-event-type expiration times in milliseconds. */ + perEventTypeExpiration?: Record; + /** Whether expiration checking is enabled (default: true). */ + enabled: boolean; +} + +/** + * Safety limits for the historical backfill that runs when the listener + * starts without a stored cursor (first boot or after downtime). + */ +export interface BackfillConfig { + /** + * Maximum number of ledgers to replay from the network tip on a cold start. + * + * When the subscriber has no persisted cursor for a contract it would + * normally request events from ledger 1, which can be an arbitrarily large + * range after downtime or a configuration change. This limit caps the + * range to the most recent `maxLedgers` ledgers instead. + * + * Set to `0` to disable the limit and allow full historical replay + * (the previous default behaviour). Default: 10 000. + */ + maxLedgers: number; +} + diff --git a/_cargo_test.txt b/_cargo_test.txt new file mode 100644 index 00000000..73207784 --- /dev/null +++ b/_cargo_test.txt @@ -0,0 +1,41 @@ +cargo :  Compiling proc-macro2 v1.0.106 +At line:15 char:3417 ++ ... pgrep\bin'; cargo test --manifest-path contract/Cargo.toml 2>&1 | Tee ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: ( Com...macro2 v1.0.106:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + Compiling quote v1.0.44 + Compiling typenum v1.19.0 + Compiling serde_core v1.0.228 + Compiling zmij v1.0.17 + Compiling serde v1.0.228 + Compiling serde_json v1.0.149 + Compiling generic-array v0.14.9 + Compiling zerocopy v0.8.34 + Compiling num-traits v0.2.19 +error: linker `link.exe` not found + | + = note: program not found + +note: the msvc targets depend on the msvc linker but `link.exe` was not +found + +note: please ensure that Visual Studio 2017 or later, or Build Tools for +Visual Studio were installed with the Visual C++ option. + +note: VS Code is a different product, and is not sufficient. + +error: could not compile `num-traits` (build script) due to 1 previous error +warning: build failed, waiting for other jobs to finish... +error: could not compile `serde_json` (build script) due to 1 previous error +error: could not compile `num-traits` (build script) due to 1 previous error +error: could not compile `generic-array` (build script) due to 1 previous error +error: could not compile `zmij` (build script) due to 1 previous error +error: could not compile `quote` (build script) due to 1 previous error +error: could not compile `generic-array` (build script) due to 1 previous error +error: could not compile `proc-macro2` (build script) due to 1 previous error +error: could not compile `serde` (build script) due to 1 previous error +error: could not compile `serde_core` (build script) due to 1 previous error +error: could not compile `zerocopy` (build script) due to 1 previous error +error: could not compile `typenum` (build script) due to 1 previous error diff --git a/_cargo_test_gnu.txt b/_cargo_test_gnu.txt new file mode 100644 index 00000000..47cc9a39 --- /dev/null +++ b/_cargo_test_gnu.txt @@ -0,0 +1,925 @@ +cargo :  Compiling serde_derive v1.0.228 +At line:15 char:3417 ++ ... pgrep\bin'; cargo +1.85.0-x86_64-pc-windows-gnu test --manifest-path ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: ( Com...derive v1.0.228:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + Compiling darling_macro v0.21.3 + Compiling generic-array v0.14.9 + Compiling der v0.7.10 + Compiling ark-ff-macros v0.4.2 + Compiling derivative v2.2.0 + Compiling thiserror-impl v1.0.69 + Compiling num-derive v0.4.2 + Compiling derive_arbitrary v1.3.2 + Compiling wasmparser-nostd v0.100.2 + Compiling curve25519-dalek-derive v0.1.1 + Compiling darling_macro v0.20.11 + Compiling darling v0.21.3 + Compiling soroban-builtin-sdk-macros v23.0.1 + Compiling arbitrary v1.3.2 + Compiling hex-literal v0.4.1 + Compiling heapless v0.8.0 + Compiling tempfile v3.25.0 + Compiling crypto-common v0.1.6 + Compiling block-buffer v0.10.4 + Compiling sec1 v0.7.3 + Compiling digest v0.10.7 + Compiling serde_with_macros v3.16.1 + Compiling crypto-bigint v0.5.5 + Compiling sha2 v0.10.9 + Compiling signature v2.2.0 + Compiling hmac v0.12.1 + Compiling ark-serialize v0.4.2 + Compiling thiserror v1.0.69 + Compiling curve25519-dalek v4.1.3 + Compiling rfc6979 v0.4.0 + Compiling ed25519 v2.2.3 + Compiling sha3 v0.10.8 + Compiling darling v0.20.11 + Compiling dtor v0.1.1 + Compiling macro-string v0.1.4 + Compiling ctor-proc-macro v0.0.6 + Compiling quick-error v1.2.3 + Compiling ark-ff v0.4.2 + Compiling bit-vec v0.8.0 + Compiling heck v0.5.0 + Compiling wait-timeout v0.2.1 + Compiling rand_chacha v0.9.0 + Compiling rusty-fork v0.3.1 + Compiling rand_xorshift v0.4.0 + Compiling ctor v0.5.0 + Compiling bit-set v0.8.0 + Compiling rand v0.9.4 + Compiling bytes-lit v0.0.5 + Compiling visibility v0.1.1 + Compiling regex-syntax v0.8.11 + Compiling ed25519-dalek v2.2.0 + Compiling serde v1.0.228 + Compiling bitflags v2.13.0 + Compiling unarray v0.1.4 + Compiling soroban-wasmi v0.31.1-soroban.20.0.1 + Compiling elliptic-curve v0.13.8 + Compiling ecdsa v0.16.9 + Compiling primeorder v0.13.6 + Compiling crate-git-revision v0.0.6 + Compiling schemars v0.8.22 + Compiling hex v0.4.3 + Compiling p256 v0.13.2 + Compiling k256 v0.13.4 + Compiling stellar-strkey v0.0.13 + Compiling stellar-xdr v23.0.0 + Compiling soroban-env-common v23.0.1 + Compiling stellar-strkey v0.0.16 + Compiling soroban-sdk v23.5.0 + Compiling proptest v1.11.0 + Compiling serde_with v3.16.1 + Compiling ark-poly v0.4.2 + Compiling ark-ec v0.4.2 + Compiling ark-bls12-381 v0.4.0 + Compiling soroban-spec v23.5.0 + Compiling soroban-env-macros v23.0.1 + Compiling soroban-spec-rust v23.5.0 + Compiling soroban-sdk-macros v23.5.0 + Compiling soroban-env-host v23.0.1 + Compiling soroban-ledger-snapshot v23.5.0 + Compiling hello-world v0.0.0 (C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\contract\contracts\hello-world) +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\audit_log_test.rs:15:30 + | +15 | use soroban_sdk::testutils::{Address as _, +Events, Ledger}; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: unused import: `NotificationCategory` + --> contracts\hello-world\src\tests\batch_ack_test.rs:9:27 + | +9 | use crate::base::events::{NotificationCategory, +NotificationPriority}; + | ^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `Val` + --> contracts\hello-world\src\tests\batch_ack_test.rs:14:69 + | +14 | use soroban_sdk::{Address, BytesN, Env, String, +Symbol, TryFromVal, Val, Vec}; + | +^^^ + +warning: unused import: `Val` + --> contracts\hello-world\src\tests\batch_event_test.rs:6:47 + | +6 | use soroban_sdk::{BytesN, Symbol, TryFromVal, +Val}; + | ^^^ + +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\batch_notification_test.rs:15:30 + | +15 | use soroban_sdk::testutils::{Address as _, +Events, Ledger}; + | ^^^^^^^^^^^^ + +warning: unused import: `AutoShareContract` + --> contracts\hello-world\src\tests\event_regression_test.rs:15:13 + | +15 | use crate::{AutoShareContract, +AutoShareContractClient}; + | ^^^^^^^^^^^^^^^^^ + +warning: unused import: `vec` + --> contracts\hello-world\src\tests\event_regression_test.rs:18:5 + | +18 |  vec, Address, BytesN, Env, String, Symbol, +TryIntoVal, Val, Vec, + | ^^^ + +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\notification_lifetime_test.rs:21:30 + | +21 | use soroban_sdk::testutils::{Address as _, +Ledger}; + | ^^^^^^^^^^^^ + +warning: unused import: `crate::base::errors::Error` + --> contracts\hello-world\src\tests\notification_test.rs:18:5 + | +18 | use crate::base::errors::Error; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `soroban_sdk::Val` + --> contracts\hello-world\src\tests\payload_validation_test.rs:600:13 + | +600 |  use soroban_sdk::Val; + | ^^^^^^^^^^^^^^^^ + +warning: unused import: `AutoShareContract` + --> contracts\hello-world\src\tests\storage_optimization_test.rs:64:17 + | +64 |  use crate::{AutoShareContract, +AutoShareContractClient}; + | ^^^^^^^^^^^^^^^^^ + +warning: unused imports: `BytesN`, `Env`, and `String` + --> contracts\hello-world\src\tests\storage_optimization_test.rs:65:57 + | +65 |  use soroban_sdk::{testutils::Address as _, +Address, BytesN, Env, String, Vec}; + | +^^^^^^ ^^^ ^^^^^^ + +warning: unused import: `crate::base::errors::Error` + --> contracts\hello-world\src\tests\subscription_cancellation_test.rs:10:5 + | +10 | use crate::base::errors::Error; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\subscription_cancellation_test.rs:15:30 + | +15 | use soroban_sdk::testutils::{Address as _, +Events}; + | ^^^^^^^^^^^^ + +warning: unused import: `crate::base::errors::Error` + --> contracts\hello-world\src\tests\template_registry_test.rs:12:9 + | +12 |  use crate::base::errors::Error; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:143:28 + | +143 |  title: +String::from_slice(&soroban_sdk::Env::default(), "Test"), + | ^^^^^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:154:28 + | +154 |  title: +String::from_slice(&soroban_sdk::Env::default(), ""), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:166:21 + | +166 |  String::from_slice(&env, +&"a".repeat(MAX_METADATA_STRING_LENGTH as usize + 1)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:180:21 + | +180 |  String::from_slice(&env, +&"d".repeat(MAX_METADATA_STRING_LENGTH as usize + 1)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:182:28 + | +182 |  title: String::from_slice(&env, +"ok"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:194:21 + | +194 |  String::from_slice(&env, +&"u".repeat(MAX_METADATA_STRING_LENGTH as usize + 1)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:196:28 + | +196 |  title: String::from_slice(&env, +"ok"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:211:31 + | +211 |  let key = String::from_slice(&env, +core::str::from_utf8(&key_bytes).unwrap()); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:212:31 + | +212 |  let val = String::from_slice(&env, +"v"); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:217:28 + | +217 |  title: String::from_slice(&env, +"ok"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:230:21 + | +230 |  String::from_slice(&env, +"priority"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:231:21 + | +231 |  String::from_slice(&env, "high"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:234:28 + | +234 |  title: String::from_slice(&env, +"Alert"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:235:39 + | +235 |  description: +Some(String::from_slice(&env, "Body")), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:236:36 + | +236 |  data_uri: +Some(String::from_slice(&env, "ipfs://abc")), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:250:31 + | +250 |  let key = String::from_slice(&env, +core::str::from_utf8(&key_bytes).unwrap()); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:251:31 + | +251 |  let val = String::from_slice(&env, +&"x".repeat(256)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:256:28 + | +256 |  title: String::from_slice(&env, +&"t".repeat(256)), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:257:39 + | +257 |  description: +Some(String::from_slice(&env, &"d".repeat(256))), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:258:36 + | +258 |  data_uri: +Some(String::from_slice(&env, &"u".repeat(256))), + | ^^^^^^^^^^ + +warning: use of deprecated method `soroban_sdk::Env::register_contract`: use +`register` + --> contracts\hello-world\src\tests\batch_ack_test.rs:162:14 + | +162 |  &env.register_contract(None, +crate::AutoShareContract), + | ^^^^^^^^^^^^^^^^^ + +warning: unused variable: `caller` + --> contracts\hello-world\src\autoshare_logic.rs:1745:5 + | +1745 |  caller: &Address, + | ^^^^^^ help: if this +is intentional, prefix it with an underscore: `_caller` + | + = note: `#[warn(unused_variables)]` on by default + +warning: variable does not need to be mutable + --> contracts\hello-world\src\channel_logic.rs:108:9 + | +108 |  let mut subscribers = load_subscribers(&env, +&channel_id); + | ----^^^^^^^^^^^ + | | + | help: remove this `mut` + | + = note: `#[warn(unused_mut)]` on by default + +warning: function `topics_of` is never used + --> contracts\hello-world\src\tests\archive_notification_test.rs:28:4 + | +28 | fn topics_of(env: &Env, event_name: &str) -> +Option> { + | ^^^^^^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: constant `ONE_HOUR` is never used + --> contracts\hello-world\src\tests\fuzz_test.rs:14:7 + | +14 | const ONE_HOUR: u64 = 3_600; + | ^^^^^^^^ + +warning: function `data_of` is never used + --> contracts\hello-world\src\tests\revocation_test.rs:56:4 + | +56 | fn data_of(env: &Env, event_name: &str) -> +Option { + | ^^^^^^^ + +warning: function `test_revoke_notification_while_contract_paused_fails` is +never used + --> contracts\hello-world\src\tests\revocation_test.rs:244:4 + | +244 | fn +test_revoke_notification_while_contract_paused_fails() { + | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `hello-world` (lib test) generated 42 warnings (run `cargo fix --lib -p hello-world +--tests` to apply 16 suggestions) + Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`test` +profile [unoptimized + debuginfo]]8;;\ target(s) in 5m 09s + Running unittests src\lib.rs (contract\target\debug\deps\hello_world-d4fae4c6ce77434b.exe) + +running 502 tests +test base::reputation::tests::test_reputation_score_calculation ... ok +test base::reputation::tests::test_reputation_tier_classification ... ok +test base::metadata_validation::tests::test_long_data_uri_invalid ... ok +test base::metadata_validation::tests::test_long_description_invalid ... ok +test base::metadata_validation::tests::test_long_title_invalid ... ok +test base::metadata_validation::tests::test_empty_title_invalid ... ok +test base::metadata_validation::tests::test_valid_metadata ... ok +test base::reputation::tests::test_sender_reputation_tracking ... ok +test base::metadata_validation::tests::test_valid_custom_fields ... ok +test base::metadata_validation::tests::test_too_many_custom_fields_invalid ... ok +test base::metadata_validation::tests::test_oversized_metadata_rejected ... ok +test tests::access_control_test::admin_only::test_configure_notification_limits_unauthorized_emits_authorization_failure_event ... ok +test tests::access_control_test::admin_only::test_pause_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_register_category_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_remove_supported_token_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_configure_notification_limits_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_pause_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_register_category_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_remove_supported_token_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_add_supported_token_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_add_supported_token_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_pause_random_group_creator_not_admin_panics - should panic ... ok +test tests::access_control_test::admin_only::test_configure_notification_limits_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_set_usage_fee_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_unpause_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_set_usage_fee_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_transfer_admin_authorized_succeeds ... ok +test tests::access_control_test::admin_only::test_transfer_admin_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_withdraw_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_withdraw_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_unpause_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::authorization_boundary::test_get_notification_cross_user_read_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_extend_notification_expiry_cross_user_write_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_get_notification_owner_can_read ... ok +test tests::access_control_test::authorization_boundary::test_revoke_notification_cross_user_write_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_get_notification_admin_can_read ... ok +test tests::access_control_test::authorization_boundary::test_cancel_notification_cross_user_write_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_ownership_validation_allows_owner_on_write ... ok +test tests::access_control_test::authorization_boundary::test_ownership_validation_rejects_non_owner_on_write ... ok +test tests::access_control_test::creator_only::test_deactivate_group_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_only::test_deactivate_group_creator_succeeds ... ok +test tests::access_control_test::creator_only::test_add_group_member_non_creator_panics - should panic ... ok +test tests::access_control_test::authorization_boundary::test_notification_ownership_is_recorded_on_schedule ... ok +test tests::access_control_test::creator_only::test_add_group_member_creator_succeeds ... ok +test tests::access_control_test::authorization_boundary::test_cross_user_access_emits_authorization_failure_event ... ok +test tests::access_control_test::authorization_boundary::test_user_cannot_access_other_users_notification ... ok +test tests::access_control_test::authorization_boundary::test_is_notification_revoked_cross_user_read_is_rejected ... ok +test tests::access_control_test::creator_or_admin_notifications::test_reduce_usage_admin_succeeds ... ok +test tests::access_control_test::creator_only::test_activate_group_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_unauthorized_emits_authorization_failure_event ... ok +test tests::access_control_test::creator_only::test_update_members_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_reduce_usage_unrelated_user_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_unrelated_user_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_extend_notification_expiry_admin_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_creator_succeeds ... ok +test tests::access_control_test::creator_only::test_activate_group_creator_succeeds ... ok +test tests::access_log_test::test_access_event_fails_for_nonexistent_notification - should panic ... ok +test tests::access_log_test::test_cross_user_access_rejected_for_nonexistent_notification - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_extend_notification_expiry_unrelated_user_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_admin_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_creator_succeeds ... ok +test tests::access_log_test::test_access_event_emitted_for_existing_notification ... ok +test tests::access_log_test::test_cross_user_read_access_is_rejected - should panic ... ok +test tests::access_log_test::test_owner_can_modify_own_notification ... ok +test tests::access_log_test::test_resource_ownership_is_validated_for_non_owner - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_unrelated_user_panics - should panic ... ok +test tests::access_log_test::test_resource_ownership_is_validated_for_owner ... ok +test tests::access_log_test::test_cross_user_write_access_is_rejected - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_extend_notification_expiry_creator_succeeds ... ok +test tests::access_log_test::test_owner_cannot_modify_other_users_notification - should panic ... ok +test tests::access_log_test::test_multiple_access_events_can_be_emitted ... ok +test tests::archive_notification_test::test_expire_archives_notification ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_admin_succeeds ... ok +test tests::audit_log_test::test_acknowledgment_blocked_when_paused ... ok +test tests::audit_log_test::test_acknowledgment_recorded ... ok +test tests::audit_log_test::test_audit_log_immutability ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_unauthorized_emits_authorization_failure_event ... ok +test tests::audit_log_test::test_audit_records_for_unknown_notification_returns_empty ... ok +test tests::archive_notification_test::test_delivery_archives_notification ... ok +test tests::audit_log_test::test_audit_sequence_numbers_increment ... ok +test tests::audit_log_test::test_cancel_notification_creates_audit_record ... ok +test tests::audit_log_test::test_delivery_attempt_blocked_when_paused ... ok +test tests::archive_notification_test::test_cancel_archives_notification ... ok +test tests::audit_log_test::test_schedule_notification_creates_audit_record ... ok +test tests::audit_log_test::test_expire_notification_creates_audit_record ... ok +test tests::audit_log_test::test_delivery_attempt_recorded ... ok +test tests::audit_log_test::test_delivery_failure_blocked_when_paused ... ok +test tests::audit_log_test::test_full_lifecycle_audit_trail ... ok +test tests::autoshare_test::test_add_duplicate_token_fails - should panic ... ok +test tests::audit_log_test::test_delivery_failure_recorded ... ok +test tests::audit_log_test::test_schedule_notification_emits_audit_event ... ok +test tests::audit_log_test::test_audit_records_filtered_by_notification_id ... ok +test tests::audit_log_test::test_batch_schedule_creates_audit_records ... ok +test tests::autoshare_test::test_add_and_get_supported_tokens ... ok +test tests::autoshare_test::test_add_member_to_non_existent_group - should panic ... ok +test tests::autoshare_test::test_activating_already_active_group_fails - should panic ... ok +test tests::autoshare_test::test_add_supported_token_fails_already_exists - should panic ... ok +test tests::autoshare_test::test_admin_initialization ... ok +test tests::autoshare_test::test_add_group_member_success ... ok +test tests::autoshare_test::test_admin_functions_after_transfer ... ok +test tests::autoshare_test::test_admin_can_pause ... ok +test tests::autoshare_test::test_admin_can_unpause ... ok +test tests::autoshare_test::test_add_group_member_unauthorized - should panic ... ok +test tests::autoshare_test::test_add_duplicate_member - should panic ... ok +test tests::autoshare_test::test_add_multiple_members_sequentially ... ok +test tests::autoshare_test::test_add_member_invalid_total_percentage - should panic ... ok +test tests::autoshare_test::test_admin_can_deactivate_channel_even_if_not_creator ... ok +test tests::autoshare_test::test_admin_can_withdraw ... ok +test tests::autoshare_test::test_create_fails_name_too_long - should panic ... ok +test tests::autoshare_test::test_create_fails_invalid_usage_count_zero - should panic ... ok +test tests::autoshare_test::test_add_member_to_inactive_group ... ok +test tests::autoshare_test::test_create_group_with_zero_usages_fails ... ok +test tests::autoshare_test::test_create_fails_duplicate_member - should panic ... ok +test tests::autoshare_test::test_create_fails_unsupported_token - should panic ... ok +test tests::autoshare_test::test_create_fails_empty_members - should panic ... ok +test tests::autoshare_test::test_get_admin ... ok +test tests::autoshare_test::test_create_fails_invalid_percentage - should panic ... ok +test tests::autoshare_test::test_creator_can_deactivate_group ... ok +test tests::autoshare_test::test_deactivated_channel_blocks_new_subscriptions ... ok +test tests::autoshare_test::test_get_all_groups_empty ... ok +test tests::autoshare_test::test_creator_can_activate_group ... ok +test tests::autoshare_test::test_create_and_get_success ... ok +test tests::autoshare_test::test_get_contract_balance ... ok +test tests::autoshare_test::test_duplicate_id_fails - should panic ... ok +test tests::autoshare_test::test_get_groups_by_creator_empty ... ok +test tests::autoshare_test::test_get_group_members_non_existent_group - should panic ... ok +test tests::autoshare_test::test_create_group_with_payment ... ok +test tests::autoshare_test::test_get_group_members_empty ... ok +test tests::autoshare_test::test_initialize_with_admin ... ok +test tests::autoshare_test::test_is_group_active_on_nonexistent_group_fails - should panic ... ok +test tests::autoshare_test::test_get_group_members_multiple ... ok +test tests::autoshare_test::test_get_all_groups_includes_inactive ... ok +test tests::autoshare_test::test_deactivating_already_inactive_group_fails - should panic ... ok +test tests::autoshare_test::test_group_created_as_active ... ok +test tests::autoshare_test::test_get_non_existent_fails - should panic ... ok +test tests::autoshare_test::test_is_group_member ... ok +test tests::autoshare_test::test_is_group_member_non_existent_group - should panic ... ok +test tests::autoshare_test::test_is_group_member_true ... ok +test tests::autoshare_test::test_is_group_member_works_on_inactive_group ... ok +test tests::autoshare_test::test_is_subscribed_false_after_channel_deactivated ... ok +test tests::autoshare_test::test_is_subscribed_false_for_non_member ... ok +test tests::autoshare_test::test_is_subscribed_on_nonexistent_channel_fails - should panic ... ok +test tests::autoshare_test::test_is_subscribed_true_for_member ... ok +test tests::autoshare_test::test_name_too_long_rejected - should panic ... ok +test tests::autoshare_test::test_name_at_max_length_succeeds ... ok +test tests::autoshare_test::test_deactivated_channel_preserves_historical_subscription_data ... ok +test tests::autoshare_test::test_non_admin_cannot_withdraw - should panic ... ok +test tests::autoshare_test::test_is_group_member_false ... ok +test tests::autoshare_test::test_non_admin_cannot_set_usage_fee - should panic ... ok +test tests::autoshare_test::test_is_subscribed_true_for_creator ... ok +test tests::autoshare_test::test_remove_supported_token_fails_not_found - should panic ... ok +test tests::autoshare_test::test_set_and_get_usage_fee ... ok +test tests::autoshare_test::test_old_admin_cannot_withdraw_after_transfer - should panic ... ok +test tests::autoshare_test::test_set_usage_fee_fails_zero - should panic ... ok +test tests::autoshare_test::test_get_all_groups_multiple ... ok +test tests::autoshare_test::test_remove_non_existent_token_fails - should panic ... ok +test tests::autoshare_test::test_remove_supported_token ... ok +test tests::autoshare_test::test_non_creator_cannot_activate - should panic ... ok +test tests::autoshare_test::test_non_creator_cannot_deactivate - should panic ... ok +test tests::autoshare_test::test_reduce_usage_fails_no_usages_remaining - should panic ... ok +test tests::autoshare_test::test_get_groups_by_creator_multiple ... ok +test tests::autoshare_test::test_status_change_on_nonexistent_group_fails - should panic ... ok +test tests::autoshare_test::test_transfer_admin_unauthorized - should panic ... ok +test tests::autoshare_test::test_transfer_admin ... ok +test tests::autoshare_test::test_members_at_max_succeeds ... ok +test tests::autoshare_test::test_withdraw_insufficient_balance - should panic ... ok +test tests::autoshare_test::test_topup_fails_unsupported_token - should panic ... ok +test tests::autoshare_test::test_update_members_fails_too_many - should panic ... ok +test tests::autoshare_test::test_viewing_inactive_group_works ... ok +test tests::autoshare_test::test_withdraw_negative_amount - should panic ... ok +test tests::autoshare_test::test_too_many_members_rejected - should panic ... ok +test tests::autoshare_test::test_withdraw_zero_amount - should panic ... ok +test tests::autoshare_test::test_topup_fails_invalid_usage_count_zero - should panic ... ok +test tests::autoshare_test::test_updating_inactive_group_fails - should panic ... ok +test tests::batch_ack_test::test_acknowledge_revoked_fails - should panic ... ok +test tests::autoshare_test::test_update_members_unauthorized - should panic ... ok +test tests::autoshare_test::test_update_members_invalid_percentage - should panic ... ok +test tests::batch_event_test::test_emit_batch_processing_completed_event ... ok +test tests::autoshare_test::test_update_members_success ... ok +test tests::batch_notification_test::test_batch_duplicate_id_rejected ... ok +test tests::batch_ack_test::test_acknowledge_expired_fails - should panic ... ok +test tests::batch_notification_test::test_batch_blocked_when_contract_paused ... ok +test tests::batch_ack_test::test_acknowledge_unauthorized_fails - should panic ... ok +test tests::batch_ack_test::test_acknowledge_multiple_notifications ... ok +test tests::batch_notification_test::test_batch_all_or_nothing_on_validation_failure ... ok +test tests::batch_notification_test::test_batch_empty_ids_rejected ... ok +test tests::batch_notification_test::test_batch_exceeding_max_size_rejected ... ok +test tests::batch_notification_test::test_batch_mismatched_lengths_rejected ... ok +test tests::batch_notification_test::test_batch_id_already_scheduled_rejected ... ok +test tests::batch_notification_test::test_batch_zero_ttl_rejected ... ok +test tests::category_registry_test::test_default_registry_has_defaults ... ok +test tests::category_registry_test::test_admin_can_register_category ... ok +test tests::batch_notification_test::test_batch_creates_all_notifications ... ok +test tests::batch_notification_test::test_batch_single_notification ... ok +test tests::batch_notification_test::test_batch_emits_summary_event ... ok +test tests::category_registry_test::test_registry_queries_multiple_categories ... ok +test tests::category_registry_test::test_non_admin_cannot_register_category - should panic ... ok +test tests::category_registry_test::test_duplicate_category_registration_rejected - should panic ... ok +test tests::category_registry_test::test_register_category_emits_event ... ok +test tests::batch_notification_test::test_batch_summary_event_has_notification_category ... ok +test tests::batch_notification_test::test_batch_emits_per_notification_events ... ok +test tests::batch_notification_test::test_batch_notifications_expire_correctly ... ok +test tests::channel_subscription_test::test_batch_subscribe_empty_rejected ... ok +test tests::channel_metadata_test::test_update_channel_metadata_emits_event ... ok +test tests::channel_subscription_test::test_batch_subscribe_too_large_rejected ... ok +test tests::channel_subscription_test::test_get_channel_creator_not_found ... ok +test tests::channel_subscription_test::test_batch_subscribe_partial_failure_does_not_corrupt_state ... ok +test tests::event_regression_test::test_admin_transferred_event_structure ... ok +test tests::channel_subscription_test::test_batch_subscribe_multiple_channels ... ok +test tests::channel_metadata_test::test_update_channel_metadata_stores_description ... ok +test tests::channel_subscription_test::test_create_channel_stores_creator ... ok +test tests::channel_subscription_test::test_subscriber_count_updates_on_subscribe_and_unsubscribe ... ok +test tests::channel_subscription_test::test_duplicate_subscribe_rejected_count_unchanged ... ok +test tests::batch_ack_test::benchmark_gas_usage ... ok +test tests::event_regression_test::test_contract_paused_event_no_data_payload ... ok +test tests::event_regression_test::test_event_names_are_stable ... ok +test tests::channel_metadata_test::test_update_channel_metadata_rejects_non_creator - should panic ... ok +test tests::event_regression_test::test_contract_paused_event_structure ... ok +test tests::event_regression_test::test_admin_transferred_event_topic_ordering ... ok +test tests::channel_metadata_test::test_update_channel_metadata_leaves_subscribers_intact ... ok +test tests::event_regression_test::test_autoshare_created_event_topic_ordering ... ok +test tests::event_regression_test::test_autoshare_updated_event_structure ... ok +test tests::event_regression_test::test_autoshare_created_event_structure ... ok +test tests::event_regression_test::test_contract_unpaused_event_structure ... ok +test tests::expiration_test::test_cancellation_blocked_when_contract_paused ... ok +test tests::event_regression_test::test_multiple_events_in_sequence ... ok +test tests::event_regression_test::test_group_activated_event_structure ... ok +test tests::event_regression_test::test_no_event_on_read_operations ... ok +test tests::expiration_test::test_cancelling_untracked_id_still_emits_event ... ok +test tests::event_regression_test::test_group_deactivated_event_structure ... ok +test tests::expiration_test::test_cannot_extend_expired_notification - should panic ... ok +test tests::expiration_test::test_cannot_extend_nonexistent_notification - should panic ... ok +test tests::expiration_test::test_cannot_extend_by_zero_seconds - should panic ... ok +test tests::expiration_test::test_expire_unknown_notification_fails ... ok +test tests::expiration_test::test_duplicate_schedule_is_rejected ... ok +test tests::event_regression_test::test_withdrawal_event_structure ... ok +test tests::expiration_test::test_expire_after_deadline_emits_event_and_reaps_storage ... ok +test tests::expiration_test::test_expire_before_deadline_is_rejected ... ok +test tests::expiration_test::test_get_unknown_notification_fails ... ok +test tests::expiration_test::test_extend_notification_expiry_by_unauthorized_user_fails - should panic ... ok +test tests::expiration_test::test_extend_notification_expiry_by_creator ... ok +test tests::expiration_test::test_extend_notification_expiry_while_contract_paused_fails - should panic ... ok +test tests::expiration_test::test_schedule_emits_notification_scheduled_event ... ok +test tests::expiration_test::test_zero_duration_is_rejected ... ok +test tests::expiration_test::test_schedule_blocked_when_contract_paused ... ok +test tests::event_regression_test::test_withdrawal_event_topic_ordering ... ok +test tests::expiration_test::test_schedule_stores_created_and_expiry ... ok +test tests::expiration_test::test_valid_notification_can_be_cancelled_and_emits_event ... ok +test tests::expiration_test::test_cannot_extend_revoked_notification - should panic ... ok +test tests::expiration_test::test_not_expired_before_deadline_and_expired_after ... ok +test tests::extended_coverage_test::test_audit_record_appended_event_has_notification_category ... ok +test tests::extended_coverage_test::test_audit_record_appended_carries_correct_action_topic ... ok +test tests::extended_coverage_test::test_audit_log_seq_starts_at_one ... ok +test tests::expiration_test::test_expired_notification_cannot_be_cancelled ... ok +test tests::expiration_test::test_extend_notification_expiry_by_admin ... ok +test tests::extended_coverage_test::test_audit_records_per_notification_match_full_log_subset ... ok +test tests::extended_coverage_test::test_configure_limits_non_admin_rejected ... ok +test tests::extended_coverage_test::test_configure_limits_valid_values_accepted ... ok +test tests::extended_coverage_test::test_batch_ttl_overflow_rejected ... ok +test tests::extended_coverage_test::test_configure_limits_min_greater_than_max_rejected ... ok +test tests::extended_coverage_test::test_acknowledgment_on_expired_notification_rejected ... ok +test tests::extended_coverage_test::test_category_registered_event_carries_category_and_priority ... ok +test tests::extended_coverage_test::test_configure_limits_emits_event_with_admin_category ... ok +test tests::extended_coverage_test::test_configure_limits_zero_batch_size_rejected ... ok +test tests::extended_coverage_test::test_record_acknowledgment_on_unknown_id_rejected ... ok +test tests::extended_coverage_test::test_notification_recalled_event_has_notification_category ... ok +test tests::extended_coverage_test::test_notification_expired_event_has_notification_category ... ok +test tests::extended_coverage_test::test_audit_log_grows_across_independent_notifications ... ok +test tests::extended_coverage_test::test_ownership_transfer_events_have_admin_category ... ok +test tests::extended_coverage_test::test_delivery_attempt_on_revoked_notification_rejected ... ok +test tests::extended_coverage_test::test_record_delivery_attempt_on_unknown_id_rejected ... ok +test tests::extended_coverage_test::test_schema_version_set_has_admin_category ... ok +test tests::extended_coverage_test::test_record_delivery_failure_on_unknown_id_rejected ... ok +test tests::extended_coverage_test::test_notification_accessed_has_notification_category ... ok +test tests::extended_coverage_test::test_revoke_notification_event_has_notification_category ... ok +test tests::extended_coverage_test::test_notification_delivered_event_has_notification_category ... ok +test tests::metadata_validation_test::test_batch_rejects_empty_title - should panic ... ok +test tests::metadata_validation_test::test_schedule_rejects_empty_title - should panic ... ok +test tests::extended_coverage_test::test_subscription_cancelled_has_group_category ... ok +test tests::metadata_validation_test::test_schedule_accepts_valid_title ... ok +test tests::mock_token_test::test_insufficient_balance - should panic ... ok +test tests::mock_token_test::test_invalid_mint_amount - should panic ... ok +test tests::fuzz_test::fuzz_reduce_usage_never_exceeds_paid_total ... ok +test tests::mock_token_test::test_invalid_transfer_amount - should panic ... ok +test tests::notification_lifetime_test::test_batch_schedule_zero_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_batch_schedule_one_second_over_max_rejected ... ok +test tests::mock_token_test::test_mock_token ... ok +test tests::notification_lifetime_test::test_batch_schedule_at_max_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_extend_by_absurd_seconds_rejected ... ok +test tests::notification_lifetime_test::test_schedule_absurd_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_extend_to_exactly_max_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_extend_one_second_over_max_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_schedule_at_max_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_schedule_minimum_valid_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_schedule_zero_lifetime_rejected ... ok +test tests::notification_test::test_admin_transfer_event_has_admin_category ... ok +test tests::notification_lifetime_test::test_extend_by_zero_seconds_rejected ... ok +test tests::notification_test::test_batch_cancel_rejects_cross_user_access ... ok +test tests::notification_lifetime_test::test_schedule_one_second_over_max_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_schedule_typical_one_hour_lifetime_succeeds ... ok +test tests::notification_test::test_cancellation_blocked_when_contract_paused ... ok +test tests::notification_test::test_cancellation_event_data_contains_notification_id ... ok +test tests::notification_test::test_batch_schedule_notifications_stores_per_item_priority ... ok +test tests::notification_test::test_cancellation_event_has_notification_category ... ok +test tests::notification_test::test_cancellation_event_is_emitted ... ok +test tests::notification_test::test_cancel_notification_rejects_cross_user_write ... ok +test tests::notification_test::test_cancel_notification_allows_owner ... ok +test tests::notification_test::test_confirm_delivery_is_idempotent_for_repeated_requests ... ok +test tests::notification_test::test_confirm_delivery_rejects_cross_user_write ... ok +test tests::notification_test::test_created_event_backward_compatible_shape ... ok +test tests::notification_test::test_created_event_has_group_category ... ok +test tests::notification_test::test_cancellation_event_topic_shape ... ok +test tests::notification_test::test_concurrent_duplicate_delivery_requests_are_safe ... ok +test tests::notification_test::test_get_notification_rejects_cross_user_read ... ok +test tests::notification_test::test_delivery_idempotency_state_is_persisted ... ok +test tests::notification_test::test_events_can_be_filtered_by_category ... ok +test tests::notification_test::test_multiple_cancellations_emit_distinct_events ... ok +test tests::notification_test::test_deactivate_and_activate_events_have_group_category ... ok +test tests::batch_notification_test::test_batch_exactly_max_size_accepted ... ok +test tests::notification_test::test_pause_and_unpause_events_have_admin_category ... ok +test tests::notification_test::test_created_group_stores_standard_priority ... ok +test tests::notification_test::test_recall_notification_emits_event_for_sender ... ok +test tests::notification_test::test_recall_notification_rejects_cross_user_write ... ok +test tests::notification_test::test_recall_notification_rejects_after_delivery_confirmation ... ok +test tests::notification_test::test_recall_notification_rejects_unauthorized_sender ... ok +test tests::notification_test::test_updated_event_has_group_category ... ok +test tests::notification_test::test_withdrawal_event_has_financial_category ... ok +test tests::notification_validation_test::test_activate_already_active_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_activate_group_while_paused_is_rejected - should panic ... ok +test tests::notification_test::test_notification_ownership_is_recorded_and_enforced ... ok +test tests::notification_validation_test::test_create_name_one_over_max_length_is_rejected - should panic ... ok +test tests::notification_validation_test::test_create_with_unsupported_token_is_rejected - should panic ... ok +test tests::notification_validation_test::test_create_zero_usage_count_is_rejected - should panic ... ok +test tests::notification_test::test_schedule_notification_event_carries_assigned_priority ... ok +test tests::notification_validation_test::test_create_payload_persisted_correctly_after_event ... ok +test tests::notification_validation_test::test_add_supported_token_duplicate_is_rejected - should panic ... ok +test tests::notification_validation_test::test_remove_supported_token_not_found_is_rejected - should panic ... ok +test tests::notification_validation_test::test_reduce_usage_nonexistent_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_set_usage_fee_by_non_admin_is_rejected - should panic ... ok +test tests::notification_test::test_schedule_notification_stores_assigned_priority ... ok +test tests::fuzz_test::fuzz_notification_zero_ttl_rejected ... ok +test tests::notification_validation_test::test_create_name_at_max_length_is_accepted ... ok +test tests::notification_validation_test::test_reduce_usage_below_zero_is_rejected - should panic ... ok +test tests::notification_validation_test::test_set_usage_fee_zero_is_rejected - should panic ... ok +test tests::notification_validation_test::test_topup_nonexistent_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_topup_zero_additional_usages_is_rejected - should panic ... ok +test tests::notification_validation_test::test_topup_unsupported_token_is_rejected - should panic ... ok +test tests::notification_validation_test::test_unauthorized_activate_emits_authorization_failure_with_admin_category - should panic ... ok +test tests::notification_validation_test::test_deactivate_already_inactive_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_unauthorized_deactivate_emits_authorization_failure_with_admin_category - should panic ... ok +test tests::notification_validation_test::test_deactivate_group_while_paused_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_duplicate_address_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_empty_list_is_rejected - should panic ... ok +test tests::notification_validation_test::test_unauthorized_update_emits_authorization_failure_with_admin_category - should panic ... ok +test tests::notification_validation_test::test_update_members_inactive_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_nonexistent_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_percentages_sum_to_99_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_51_members_is_rejected - should panic ... ok +test tests::fuzz_test::fuzz_paused_contract_blocks_group_creation ... ok +test tests::notification_validation_test::test_update_members_single_member_100_percent_is_valid ... ok +test tests::notification_validation_test::test_withdrawal_negative_amount_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_while_paused_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_valid_payload_emits_group_category ... ok +test tests::notification_validation_test::test_update_members_percentages_sum_to_101_is_rejected - should panic ... ok +test tests::notification_validation_test::test_withdrawal_zero_amount_is_rejected - should panic ... ok +test tests::notification_validation_test::test_withdrawal_exceeds_balance_is_rejected - should panic ... ok +test tests::notification_version_test::test_current_notification_version_is_documented ... ok +test tests::ownership_transfer_test::test_current_owner_unchanged_until_acceptance ... ok +test tests::ownership_transfer_test::test_get_pending_owner_returns_none_initially ... ok +test tests::ownership_transfer_test::test_initiate_and_accept_ownership_transfer ... ok +test tests::ownership_transfer_test::test_accept_ownership_fails_when_no_transfer_pending - should panic ... ok +test tests::ownership_transfer_test::test_new_owner_can_exercise_admin_rights_after_transfer ... ok +test tests::ownership_transfer_test::test_pending_owner_cleared_after_acceptance ... ok +test tests::notification_version_test::test_batch_scheduled_notifications_include_version ... ok +test tests::ownership_transfer_test::test_pending_owner_set_after_initiation ... ok +test tests::ownership_transfer_test::test_self_transfer_rejected_by_initiate - should panic ... ok +test tests::ownership_transfer_test::test_transfer_admin_non_owner_rejected - should panic ... ok +test tests::notification_validation_test::test_withdrawal_event_carries_correct_amount_and_financial_category ... ok +test tests::ownership_transfer_test::test_non_owner_cannot_initiate_ownership_transfer - should panic ... ok +test tests::ownership_transfer_test::test_wrong_address_cannot_accept_ownership - should panic ... ok +test tests::notification_version_test::test_scheduled_notification_includes_version ... ok +test tests::ownership_transfer_test::test_owner_can_change_pending_owner_before_acceptance ... ok +test tests::pause_test::test_get_paused_status_view_works_before_admin_initialization ... ok +test tests::notification_validation_test::test_update_members_exactly_50_members_is_valid ... ok +test tests::ownership_transfer_test::test_transfer_admin_still_works ... ok +test tests::ownership_transfer_test::test_transfer_admin_rejects_self_transfer - should panic ... ok +test tests::pause_test::test_initial_state_is_unpaused_after_deployment ... ok +test tests::pause_test::test_admin_can_pause ... ok +test tests::pause_test::test_non_admin_pause_returns_unauthorized_error ... ok +test tests::pause_test::test_admin_can_unpause ... ok +test tests::pause_test::test_non_admin_unpause_returns_unauthorized_error ... ok +test tests::pause_test::test_pause_emits_event_with_admin_actor ... ok +test tests::pause_test::test_paused_status_returned_correctly ... ok +test tests::pause_test::test_add_member_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_repeated_state_transitions_active_paused_active_paused ... ok +test tests::pause_test::test_pause_already_paused_returns_already_paused_error ... ok +test tests::pause_test::test_unpause_already_active_returns_not_paused_error ... ok +test tests::pause_test::test_transfer_admin_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_unpause_emits_event_with_admin_actor ... ok +test tests::pause_test::test_read_functions_work_when_paused ... ok +test tests::pause_test::test_create_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_deactivate_group_returns_contract_paused_error_when_paused ... ok +test tests::payload_validation_test::test_admin_transfer_has_critical_priority ... ok +test tests::pause_test::test_update_members_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_operations_work_after_unpause ... ok +test tests::payload_validation_test::test_create_rejects_duplicate_id ... ok +test tests::payload_validation_test::test_admin_events_have_admin_category ... ok +test tests::payload_validation_test::test_create_rejects_unsupported_token ... ok +test tests::payload_validation_test::test_consumer_can_filter_by_category ... ok +test tests::payload_validation_test::test_create_rejects_name_over_max_length ... ok +test tests::pause_test::test_topup_subscription_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_state_preserved_after_unpause_cycle ... ok +test tests::payload_validation_test::test_schedule_rejects_duplicate_id ... ok +test tests::payload_validation_test::test_create_rejects_zero_usage_count ... ok +test tests::payload_validation_test::test_financial_events_have_financial_category ... ok +test tests::payload_validation_test::test_notification_events_have_notification_category ... ok +test tests::fuzz_test::fuzz_notification_ttl_positive_always_schedules ... ok +test tests::payload_validation_test::test_group_events_have_group_category ... ok +test tests::payload_validation_test::test_ttl_of_one_second_is_valid ... ok +test tests::payload_validation_test::test_usage_fee_boundary_one_is_valid ... ok +test tests::payload_validation_test::test_single_member_at_100_percent_is_valid ... ok +test tests::preferences_test::preferences_tests::test_other_user_cannot_set_channel_preference - should panic ... ok +test tests::payload_validation_test::test_schedule_rejects_overflow_ttl ... ok +test tests::payload_validation_test::test_update_members_rejects_duplicate_addresses ... ok +test tests::payload_validation_test::test_usage_fee_zero_is_rejected ... ok +test tests::preferences_test::preferences_tests::test_get_preferences_returns_defaults_for_new_user ... ok +test tests::payload_validation_test::test_update_members_rejects_empty_list ... ok +test tests::payload_validation_test::test_schedule_rejects_zero_ttl ... ok +test tests::payload_validation_test::test_every_event_carries_category_and_priority ... ok +test tests::preferences_test::preferences_tests::test_set_category_preference_disables_payment_notifications ... ok +test tests::preferences_test::preferences_tests::test_set_category_preference_disables_system_alerts ... ok +test tests::preferences_test::preferences_tests::test_reset_preferences_restores_all_defaults ... ok +test tests::preferences_test::preferences_tests::test_set_channel_preference_disables_email ... ok +test tests::payload_validation_test::test_update_members_rejects_percentages_not_summing_to_100 ... ok +test tests::preferences_test::preferences_tests::test_set_preferences_updates_channels_and_categories ... ok +test tests::preferences_test::preferences_tests::test_ui_can_read_all_channel_states ... ok +test tests::preferences_test::preferences_tests::test_set_preferences_emits_channel_events_for_each_channel ... ok +test tests::revocation_test::test_cannot_cancel_revoked_notification - should panic ... ok +test tests::payload_validation_test::test_update_members_rejects_over_max_members ... ok +test tests::revocation_test::test_cannot_revoke_nonexistent_notification - should panic ... ok +test tests::preferences_test::preferences_tests::test_preferences_are_per_recipient ... ok +test tests::preferences_test::preferences_tests::test_preferences_persist_after_page_refresh ... ok +test tests::revocation_test::test_cannot_revoke_expired_notification - should panic ... ok +test tests::preferences_test::preferences_tests::test_set_channel_preference_emits_channel_preference_updated ... ok +test tests::revocation_test::test_is_notification_revoked_after_revocation ... ok +test tests::revocation_test::test_revocation_stores_timestamp ... ok +test tests::revocation_test::test_cannot_expire_revoked_notification - should panic ... ok +test tests::revocation_test::test_revoke_event_has_notification_category ... ok +test tests::revocation_test::test_revoke_notification_by_admin ... ok +test tests::preferences_test::preferences_tests::test_set_channel_preference_re_enables_channel ... ok +test tests::revocation_test::test_revoke_notification_by_creator ... ok +test tests::schema_version_test::test_is_version_supported_returns_false_for_zero ... ok +test tests::schema_version_test::test_is_version_supported_returns_true_for_valid ... ok +test tests::schema_version_test::test_schema_version_default_is_zero ... ok +test tests::revocation_test::test_cannot_revoke_already_revoked_notification - should panic ... ok +test tests::schema_version_test::test_set_schema_version_previous_version_tracked ... ok +test tests::revocation_test::test_revoke_notification_emits_event ... ok +test tests::schema_version_test::test_set_schema_version_requires_admin - should panic ... ok +test tests::schema_version_test::test_is_version_supported_returns_false_for_future ... ok +test tests::schema_version_test::test_set_schema_version_rejects_zero - should panic ... ok +test tests::schema_version_test::test_set_schema_version_stores_and_emits ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_admin_operations ... ok +test tests::schema_version_test::test_set_schema_version_rejects_unsupported_version - should panic ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_pause_flag ... ok +test tests::revocation_test::test_revoke_event_has_high_priority ... ok +test tests::revocation_test::test_revoke_by_unauthorized_user_fails - should panic ... ok +test tests::revocation_test::test_revoked_notification_still_queryable ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_is_group_member_uses_embedded_storage ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_members_embedded_in_details ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_emits_event ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_event_data_contains_cancelled_at ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_usage_fee ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_supported_tokens ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_blocked_when_paused - should panic ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_update_members_single_write_consistency ... ok +test tests::template_registry_test::template_registry_tests::test_get_template_panics_for_nonexistent_id - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_duplicate_registration_panics - should panic ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_event_topic_shape ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_rejects_already_inactive_group - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_non_owner_cannot_update_template - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_multiple_templates_are_independent ... ok +test tests::template_registry_test::template_registry_tests::test_register_with_empty_content_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_register_template_succeeds ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_unknown_group_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_register_with_name_too_long_panics - should panic ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_by_member ... ok +test tests::template_registry_test::template_registry_tests::test_registered_template_can_be_retrieved ... ok +test tests::test_utils_test::test_create_zero_users ... ok +test tests::template_registry_test::template_registry_tests::test_update_nonexistent_template_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_template_exists_returns_false_for_unknown_id ... ok +test tests::template_registry_test::template_registry_tests::test_update_emits_template_updated_event ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_deactivates_group ... ok +test tests::template_registry_test::template_registry_tests::test_owner_can_update_template ... ok +test tests::template_registry_test::template_registry_tests::test_register_emits_template_registered_event ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_rejects_non_member - should panic ... ok +test tests::version_test::test_version ... ok +test tests::test_utils_test::test_mock_token_helpers ... ok +test tests::test_utils_test::test_setup_test_env ... ok +test tests::test_utils_test::test_autoshare_helpers ... ok +test tests::template_registry_test::template_registry_tests::test_template_exists_returns_true_for_registered_id ... ok +test tests::fuzz_test::fuzz_zero_usage_count_always_rejected ... ok +test tests::fuzz_test::fuzz_member_percentages_sum_to_100_succeed ... ok + +test result: ok. 502 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 17.38s + diff --git a/_dp.txt b/_dp.txt new file mode 100644 index 00000000..49b7c2d4 --- /dev/null +++ b/_dp.txt @@ -0,0 +1,163 @@ +import { + ProviderCapability, + ProviderMetadata, + NotificationProvider, + DeliveryPayload, + DeliveryResult, +} from '../../types/provider-capabilities'; +import { DiscordNotificationService, DiscordMessage } from '../discord-notification'; +import { DiscordConfig } from '../../types'; +import { sendWebhook } from '../webhook-sender'; +import logger from '../../utils/logger'; + +/** + * Capabilities declared by the Discord Webhook provider. + * + * - RICH_FORMATTING: embeds with colour, fields, timestamp, footer. + * - ATTACHMENTS: file/image uploads via multipart form (Discord Files API). + * - MESSAGE_UPDATES: PATCH /webhooks/:id/:token/messages/:msgId to edit. + * - THREADING: post into an existing forum/text thread via `thread_id`. + * - INTERACTIVE_COMPONENTS: action rows (buttons, select menus) in messages. + * + * NATIVE_SCHEDULING is not declared because Discord does not offer + * first-party scheduled message delivery; the pipeline handles scheduling. + */ +const DISCORD_CAPABILITIES = new Set([ + ProviderCapability.RICH_FORMATTING, + ProviderCapability.ATTACHMENTS, + ProviderCapability.MESSAGE_UPDATES, + ProviderCapability.THREADING, + ProviderCapability.INTERACTIVE_COMPONENTS, +]); + +/** + * Wraps `DiscordNotificationService` behind the `NotificationProvider` + * interface so the core pipeline never imports a concrete Discord class. + * + * Capability handling: + * - Requested features that Discord supports are attempted. + * - Requested features that are not in `DISCORD_CAPABILITIES` are logged + * and listed in `DeliveryResult.degradedCapabilities`; delivery still + * succeeds for the features that are supported. + */ +export class DiscordNotificationProvider implements NotificationProvider { + readonly metadata: ProviderMetadata = { + id: 'discord', + name: 'Discord Webhook', + version: '1.0.0', + capabilities: DISCORD_CAPABILITIES, + }; + + private readonly service: DiscordNotificationService; + + constructor(config: DiscordConfig, service?: DiscordNotificationService) { + this.service = service ?? new DiscordNotificationService(config); + } + + hasCapability(capability: ProviderCapability): boolean { + return this.metadata.capabilities.has(capability); + } + + async deliver(payload: DeliveryPayload): Promise { + const { payload: body, targetRecipient, requestedFeatures, requestId } = payload; + + // Collect features that were requested but are not supported. + const degradedCapabilities: ProviderCapability[] = []; + if (requestedFeatures) { + for (const feature of requestedFeatures) { + if (!this.hasCapability(feature)) { + degradedCapabilities.push(feature); + logger.warn('Discord provider: requested feature not supported — skipping', { + requestId, + feature, + provider: this.metadata.id, + }); + } + } + } + + try { + const message = this.buildMessage(body); + const response = await sendWebhook(targetRecipient, message, { timeoutMs: 5_000 }); + const providerMessageId = response.headers.get('x-message-id') ?? undefined; + const providerResponse = { statusCode: response.status }; + + if (!response.ok) { + logger.warn('Discord provider: webhook responded with non-OK status', { + requestId, + targetRecipient, + status: response.status, + }); + return { + success: false, + degradedCapabilities, + errorMessage: `HTTP ${response.status}`, + errorCode: `HTTP_${response.status}`, + statusCode: response.status, + providerMessageId, + providerResponse, + }; + } + + logger.info('Discord provider: message delivered', { requestId, targetRecipient }); + return { success: true, degradedCapabilities, providerMessageId, providerResponse }; + } catch (error) { + logger.error('Discord provider: delivery error', { + requestId, + targetRecipient, + error: error instanceof Error ? error.name : 'Unknown provider error', + }); + return { + success: false, + degradedCapabilities, + errorCode: error instanceof Error && error.name === 'AbortError' ? 'TIMEOUT' : 'TRANSPORT_ERROR', + errorMessage: error instanceof Error && error.name === 'AbortError' + ? 'Provider request timed out' + : 'Provider transport error', + }; + } + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + /** + * Build a `DiscordMessage` from the generic JSON payload. + * + * Supported payload shapes (in order of precedence): + * 1. `{ message: DiscordMessage }` — produced by + * `NotificationAPI.scheduleDiscordNotification()`. + * 2. `{ embeds: [...] }` — the caller already built an embed array. + * 3. `{ content: string }` — plain-text message. + * 4. `{ text: string }` — alias for `content`. + * 5. Fallback: serialise the entire payload as a JSON string (truncated + * to Discord's 2 000-character limit). + */ + private buildMessage(body: Record): DiscordMessage { + // Shape 1 – pre-built DiscordMessage nested under `message` key + if (body.message && typeof body.message === 'object') { + return body.message as DiscordMessage; + } + + // Shape 2 – top-level embeds array + if (Array.isArray(body.embeds)) { + return { embeds: body.embeds as DiscordMessage['embeds'] }; + } + + // Shapes 3 & 4 – plain text + const text = + typeof body.content === 'string' + ? body.content + : typeof body.text === 'string' + ? body.text + : JSON.stringify(body).slice(0, 2_000); + + return { content: text }; + } + + /** Expose deduplication / timeout metrics from the underlying service. */ + getMetrics() { + return this.service.getMetrics(); + } +} diff --git a/_dpHead.txt b/_dpHead.txt new file mode 100644 index 00000000..420dc459 --- /dev/null +++ b/_dpHead.txt @@ -0,0 +1,175 @@ +import { + ProviderCapability, + ProviderMetadata, + NotificationProvider, + DeliveryPayload, + DeliveryResult, + ProviderHealthResult, +} from '../../types/provider-capabilities'; +import { DiscordNotificationService, DiscordMessage } from '../discord-notification'; +import { DiscordConfig } from '../../types'; +import { sendWebhook } from '../webhook-sender'; +import { sanitizeCredentials } from '../../utils/credential-sanitizer'; +import logger from '../../utils/logger'; + +const DISCORD_CAPABILITIES = new Set([ + ProviderCapability.RICH_FORMATTING, + ProviderCapability.ATTACHMENTS, + ProviderCapability.MESSAGE_UPDATES, + ProviderCapability.THREADING, + ProviderCapability.INTERACTIVE_COMPONENTS, +]); + +export class DiscordNotificationProvider implements NotificationProvider { + readonly metadata: ProviderMetadata = { + id: 'discord', + name: 'Discord Webhook', + version: '1.0.0', + capabilities: DISCORD_CAPABILITIES, + }; + + private readonly service: DiscordNotificationService; + private readonly defaultWebhookUrl?: string; + + constructor(config: DiscordConfig, service?: DiscordNotificationService) { + this.service = service ?? new DiscordNotificationService(config); + this.defaultWebhookUrl = config.webhookUrl; + } + + hasCapability(capability: ProviderCapability): boolean { + return this.metadata.capabilities.has(capability); + } + + async deliver(payload: DeliveryPayload): Promise { + const { payload: body, targetRecipient, requestedFeatures, requestId } = payload; + + const degradedCapabilities: ProviderCapability[] = []; + if (requestedFeatures) { + for (const feature of requestedFeatures) { + if (!this.hasCapability(feature)) { + degradedCapabilities.push(feature); + logger.warn('Discord provider: requested feature not supported — skipping', { + requestId, + feature, + provider: this.metadata.id, + }); + } + } + } + + try { + const message = this.buildMessage(body); + const response = await sendWebhook(targetRecipient, message, { timeoutMs: 5_000 }); + const providerMessageId = response.headers.get('x-message-id') ?? undefined; + const providerResponse = { statusCode: response.status }; + + if (!response.ok) { + logger.warn('Discord provider: webhook responded with non-OK status', { + requestId, + targetRecipient: sanitizeCredentials(targetRecipient), + status: response.status, + body: sanitizeCredentials(errorText), + }); + return { + success: false, + degradedCapabilities, + errorMessage: `HTTP ${response.status}: ${sanitizeCredentials(errorText)}`, + }; + } + + logger.info('Discord provider: message delivered', { + requestId, + targetRecipient: sanitizeCredentials(targetRecipient), + }); + return { success: true, degradedCapabilities }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const sanitized = sanitizeCredentials(errorMessage); + logger.error('Discord provider: delivery error', { + requestId, + targetRecipient: sanitizeCredentials(targetRecipient), + error: sanitized, + }); + return { success: false, degradedCapabilities, errorMessage: sanitized }; + } + } + + /** + * Independently checks Discord provider reachability. + * Redacts any webhook token or credential from failure details. + */ + async checkHealth(targetRecipient?: string): Promise { + const url = targetRecipient || this.defaultWebhookUrl; + const checkedAt = new Date().toISOString(); + + if (!url) { + return { + providerId: this.metadata.id, + providerName: this.metadata.name, + status: 'not_configured', + detail: 'No Discord webhook URL configured', + checkedAt, + }; + } + + const start = Date.now(); + try { + // Discord Webhook GET endpoint returns metadata (id, name, guild_id) without posting a message + const response = await fetch(url, { method: 'GET' }); + const latencyMs = Date.now() - start; + + if (response.ok) { + return { + providerId: this.metadata.id, + providerName: this.metadata.name, + status: 'ok', + latencyMs, + checkedAt, + }; + } + + return { + providerId: this.metadata.id, + providerName: this.metadata.name, + status: 'error', + latencyMs, + detail: `HTTP ${response.status}`, + checkedAt, + }; + } catch (err: any) { + const latencyMs = Date.now() - start; + const rawDetail = err instanceof Error ? err.message : String(err); + return { + providerId: this.metadata.id, + providerName: this.metadata.name, + status: 'error', + latencyMs, + detail: sanitizeCredentials(rawDetail), + checkedAt, + }; + } + } + + private buildMessage(body: Record): DiscordMessage { + if (body.message && typeof body.message === 'object') { + return body.message as DiscordMessage; + } + + if (Array.isArray(body.embeds)) { + return { embeds: body.embeds as DiscordMessage['embeds'] }; + } + + const text = + typeof body.content === 'string' + ? body.content + : typeof body.text === 'string' + ? body.text + : JSON.stringify(body).slice(0, 2_000); + + return { content: text }; + } + + getMetrics() { + return this.service.getMetrics(); + } +} diff --git a/_ev_ba7.txt b/_ev_ba7.txt new file mode 100644 index 00000000..7478631e --- /dev/null +++ b/_ev_ba7.txt @@ -0,0 +1,137 @@ +import http from 'http'; +import { createEventsServer } from '../api/events-server'; + +const TEST_PORT = 19876; + +function makeRequest( + path: string, + options: { headers?: Record; method?: string } = {} +): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { + return new Promise((resolve, reject) => { + const req = http.request( + { hostname: '127.0.0.1', port: TEST_PORT, path, method: options.method ?? 'GET', headers: options.headers }, + (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + let body = data; + try { body = JSON.parse(data); } catch (e) {} + resolve({ status: res.statusCode ?? 0, headers: res.headers, body }); + }); + ); + req.on('error', reject); + req.end(); + }); +} + +describe('correlation ID propagation', () => { + let server: http.Server; + + beforeAll((done) => { + server = createEventsServer({ + port: TEST_PORT, + stellarRpcUrl: 'http://localhost:8000', + stellarNetworkPassphrase: 'Test SDF Network ; September 2015', + contractAddresses: [], + }); + server.listen(TEST_PORT, done); + }); + + afterAll((done) => { + server.close(done); + }); + + test('generates a correlation ID when none is provided', async () => { + const { headers } = await makeRequest('/api/events'); + expect(headers['x-correlation-id']).toBeTruthy(); + }); + + test('echoes back the caller-supplied correlation ID', async () => { + const myId = 'my-trace-abc123'; + const { headers } = await makeRequest('/api/events', { + headers: { 'x-correlation-id': myId }, + }); + expect(headers['x-correlation-id']).toBe(myId); + }); + + test('always includes x-request-id alongside correlation ID', async () => { + const { headers } = await makeRequest('/api/events'); + expect(headers['x-request-id']).toBeTruthy(); + expect(headers['x-correlation-id']).toBeTruthy(); + expect(headers['x-request-id']).not.toBe(headers['x-correlation-id']); + }); + + test('correlation ID flows through on 404 responses', async () => { + const myId = 'trace-404-test'; + const { status, headers } = await makeRequest('/no-such-route', { + headers: { 'x-correlation-id': myId }, + }); + expect(status).toBe(404); + expect(headers['x-correlation-id']).toBe(myId); + }); +}); + +describe('GET /api/events pagination', () => { + let server: http.Server; + + beforeAll((done) => { + server = createEventsServer({ + port: TEST_PORT, + stellarRpcUrl: 'http://localhost:8000', + stellarNetworkPassphrase: 'Test SDF Network ; September 2015', + contractAddresses: [], + }); + server.listen(TEST_PORT, done); + + // Add 150 mock events to the registry + const { eventRegistry } = require('../store/event-registry'); + for (let i = 0; i < 150; i++) { + eventRegistry.addEvent({ + eventId: `0000000000000000-${i}`, + contractAddress: 'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', + eventName: 'autoshare_created', + ledger: 1000 + i, + type: 'contract', + topic: ['test'], + value: 'test', + txHash: '0x123', + receivedAt: Date.now() + }); + } + }); + + afterAll((done) => { + server.close(done); + }); + + test('defaults to 20 items when limit is not provided', async () => { + const { status, body } = await makeRequest('/api/events'); + expect(status).toBe(200); + expect(body.events.length).toBe(20); + expect(body.count).toBeGreaterThanOrEqual(150); + }); + + test('respects limit when provided within bounds', async () => { + const { status, body } = await makeRequest('/api/events?limit=50'); + expect(status).toBe(200); + expect(body.events.length).toBe(50); + }); + + test('caps excessively large limit at 100', async () => { + const { status, body } = await makeRequest('/api/events?limit=200'); + expect(status).toBe(200); + expect(body.events.length).toBe(100); + }); + + test('handles negative limits by falling back to minimum (1)', async () => { + const { status, body } = await makeRequest('/api/events?limit=-10'); + expect(status).toBe(200); + expect(body.events.length).toBe(1); + }); + + test('handles invalid limit by falling back to default', async () => { + const { status, body } = await makeRequest('/api/events?limit=invalid'); + expect(status).toBe(200); + expect(body.events.length).toBe(20); + }); +}); diff --git a/_ev_d14.txt b/_ev_d14.txt new file mode 100644 index 00000000..f89c2268 --- /dev/null +++ b/_ev_d14.txt @@ -0,0 +1,127 @@ +import http from 'http'; +import { createEventsServer } from '../api/events-server'; + +const TEST_PORT = 19876; + +function makeRequest( + path: string, + options: { headers?: Record; method?: string; port?: number } = {} +): Promise<{ status: number; headers: http.IncomingHttpHeaders }> { + return new Promise((resolve, reject) => { + const req = http.request( + { hostname: '127.0.0.1', port: options.port ?? TEST_PORT, path, method: options.method ?? 'GET', headers: options.headers }, + (res) => { + res.resume(); // drain body + resolve({ status: res.statusCode ?? 0, headers: res.headers }); + } + ); + req.on('error', reject); + req.end(); + }); +} + +describe('correlation ID propagation', () => { + let server: http.Server; + + beforeAll((done) => { + server = createEventsServer({ + port: TEST_PORT, + stellarRpcUrl: 'http://localhost:8000', + stellarNetworkPassphrase: 'Test SDF Network ; September 2015', + contractAddresses: [], + }); + server.listen(TEST_PORT, done); + }); + + afterAll((done) => { + server.close(done); + }); + + test('generates a correlation ID when none is provided', async () => { + const { headers } = await makeRequest('/api/events'); + expect(headers['x-correlation-id']).toBeTruthy(); + }); + + test('echoes back the caller-supplied correlation ID', async () => { + const myId = 'my-trace-abc123'; + const { headers } = await makeRequest('/api/events', { + headers: { 'x-correlation-id': myId }, + }); + expect(headers['x-correlation-id']).toBe(myId); + }); + + test('always includes x-request-id alongside correlation ID', async () => { + const { headers } = await makeRequest('/api/events'); + expect(headers['x-request-id']).toBeTruthy(); + expect(headers['x-correlation-id']).toBeTruthy(); + expect(headers['x-request-id']).not.toBe(headers['x-correlation-id']); + }); + + test('correlation ID flows through on 404 responses', async () => { + const myId = 'trace-404-test'; + const { status, headers } = await makeRequest('/no-such-route', { + headers: { 'x-correlation-id': myId }, + }); + expect(status).toBe(404); + expect(headers['x-correlation-id']).toBe(myId); + }); +}); + +describe('security headers', () => { + let server: http.Server; + + beforeAll((done) => { + server = createEventsServer({ + port: TEST_PORT, + stellarRpcUrl: 'http://localhost:8000', + stellarNetworkPassphrase: 'Test SDF Network ; September 2015', + contractAddresses: [], + }); + server.listen(TEST_PORT, done); + }); + + afterAll((done) => { + server.close(done); + }); + + test('sets baseline headers on preflight responses without changing cache policy', async () => { + const { status, headers } = await makeRequest('/api/events', { method: 'OPTIONS' }); + + expect(status).toBe(204); + expect(headers['x-content-type-options']).toBe('nosniff'); + expect(headers['x-frame-options']).toBe('SAMEORIGIN'); + expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin'); + expect(headers['strict-transport-security']).toBeUndefined(); + expect(headers['cache-control']).toBeUndefined(); + }); + + test('sets baseline headers on not-found responses', async () => { + const { status, headers } = await makeRequest('/no-such-route'); + + expect(status).toBe(404); + expect(headers['x-content-type-options']).toBe('nosniff'); + expect(headers['x-frame-options']).toBe('SAMEORIGIN'); + expect(headers['referrer-policy']).toBe('strict-origin-when-cross-origin'); + }); + + test('sets HSTS only when production is explicitly enabled', async () => { + const productionServer = createEventsServer({ + port: 0, + isProduction: true, + stellarRpcUrl: 'http://localhost:8000', + stellarNetworkPassphrase: 'Test SDF Network ; September 2015', + contractAddresses: [], + }); + + await new Promise((resolve) => productionServer.listen(0, '127.0.0.1', resolve)); + + try { + const address = productionServer.address() as { port: number }; + const { headers } = await makeRequest('/no-such-route', { port: address.port }); + + expect(headers['strict-transport-security']).toBe('max-age=31536000'); + } finally { + await new Promise((resolve) => productionServer.close(() => resolve())); + } + }); +}); diff --git a/_jest.txt b/_jest.txt new file mode 100644 index 00000000..626d7183 --- /dev/null +++ b/_jest.txt @@ -0,0 +1,24934 @@ +node : ts-jest[ts-compiler] (WARN) src/__tests__/notification-flow-e2e.test.ts:171:11 - +error TS2304: Cannot find name 'scheduler'. +At line:15 char:3363 ++ ... \listener'; node ./node_modules/jest/bin/jest.js --maxWorkers=2 --wor ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (ts-jest[ts-comp...me 'scheduler'.:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + +171 await scheduler.stop(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:247:13 - error TS2304: Cannot +find name 'scheduler'. + +247 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:341:47 - error TS2349: This +expression is not callable. + Type '{ payload: { message: string; }; notificationType: NotificationType; targetRecipient: string; executeAt: Date; +it(: any, async: any): any; }' has no call signatures. + +341 const id = await api.scheduleNotification({ +   ~ +342 payload: { message: 'deliver me' }, +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... +345 executeAt: new Date('2026-06-24T12:00:02.000Z'), +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +346 it('should log execution attempts for audit trail', async () => { +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:346:8 - error TS7006: +Parameter '(Missing)' implicitly has an 'any' type. + +346 it('should log execution attempts for audit trail', async () => { +    +src/__tests__/notification-flow-e2e.test.ts:346:57 - error TS7006: +Parameter 'async' implicitly has an 'any' type. + +346 it('should log execution attempts for audit trail', async () => { +   ~~~~~ +src/__tests__/notification-flow-e2e.test.ts:368:11 - error TS2454: +Variable 'scheduler' is used before being assigned. + +368 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:371:23 - error TS2454: +Variable 'repository' is used before being assigned. + +371 const row = await repository.getById(id); +   ~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:371:42 - error TS2304: Cannot +find name 'id'. + +371 const row = await repository.getById(id); +   ~~ +src/__tests__/notification-flow-e2e.test.ts:373:12 - error TS2454: +Variable 'sendMock' is used before being assigned. + +373 expect(sendMock).toHaveBeenCalledTimes(1); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:389:11 - error TS2304: Cannot +find name 'scheduler'. + +389 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:394:12 - error TS2304: Cannot +find name 'sendMock'. + +394 expect(sendMock).not.toHaveBeenCalled(); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:405:11 - error TS2304: Cannot +find name 'scheduler'. + +405 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:416:12 - error TS2304: Cannot +find name 'sendMock'. + +416 expect(sendMock).toHaveBeenCalledTimes(1); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:427:11 - error TS2304: Cannot +find name 'scheduler'. + +427 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:455:11 - error TS2304: Cannot +find name 'scheduler'. + +455 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:461:12 - error TS2304: Cannot +find name 'sendMock'. + +461 expect(sendMock).toHaveBeenCalledTimes(3); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:476:11 - error TS2304: Cannot +find name 'scheduler'. + +476 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:479:12 - error TS2304: Cannot +find name 'sendMock'. + +479 expect(sendMock).toHaveBeenCalledWith( +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:501:53 - error TS2304: Cannot +find name 'idempotencyKey'. + +501 [new Date(Date.now() - 1000).toISOString(), idempotencyKey] +   ~~~~~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:507:7 - error TS2304: Cannot +find name 'stats'. + +507 stats = await idempotencyRepo.getStats(); +   ~~~~~ +src/__tests__/notification-flow-e2e.test.ts:508:14 - error TS2304: Cannot +find name 'stats'. + +508 expect(stats.expired).toBeGreaterThanOrEqual(0); +   ~~~~~ +src/__tests__/notification-flow-e2e.test.ts:907:13 - error TS2304: Cannot +find name 'scheduler'. + +907 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:346:8 - error TS1003: +Identifier expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:346:55 - error TS1138: +Parameter declaration expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~ +src/__tests__/notification-flow-e2e.test.ts:346:63 - error TS1005: ',' +expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~ +src/__tests__/notification-flow-e2e.test.ts:346:66 - error TS1005: ',' +expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~~ +src/__tests__/notification-flow-e2e.test.ts:366:6 - error TS1128: +Declaration or statement expected. + +366 }); +   ~ +src/__tests__/notification-flow-e2e.test.ts:985:1 - error TS1005: '}' +expected. + +985 +   + + src/__tests__/notification-flow-e2e.test.ts:857:63 + 857 describe('5. Backpressure and idempotency integration', () => { +    ~ + The parser expected to find a '}' to match the '{' token here. +ts-jest[ts-compiler] (WARN) src/__tests__/blockchain-to-notification-e2e.test.ts:153:34 - +error TS2345: Argument of type '{ events: never[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: never[]; cursor: string; }' is missing the following properties from type 'GetEventsResponse': +latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +153 expect(validateRpcResponse(response).valid).toBe(true); +   ~~~~~~~~ +src/__tests__/blockchain-to-notification-e2e.test.ts:867:42 - error TS2345: +Argument of type '{ events: StellarSDK.rpc.Api.EventResponse[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: EventResponse[]; cursor: string; }' is missing the following properties from type +'GetEventsResponse': latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +867 const rpcCheck = validateRpcResponse(rpcResponse); +   ~~~~~~~~~~~ +FAIL src/__tests__/notification-flow-e2e.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\__tests__\notification-flow-e2e.test.ts:302 + it(, async) { } + ^ + + SyntaxError: Unexpected token ',' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/api/events-server.test.ts + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber.test.ts:739:9 - +error TS2353: Object literal may only specify known properties, and 'receivedAt' does not exist in +type 'Partial'. + +739 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:772:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +772 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:799:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +799 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:835:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +835 receivedAt: veryOldTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:861:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +861 receivedAt: NOW - (365 * 24 * 60 * 60 * 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:884:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +884 receivedAt: NOW - (DEFAULT_EXPIRATION_MS + 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:888:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +888 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:924:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +924 receivedAt: fastEventExpiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:928:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +928 receivedAt: NOW - 60 * 60 * 1000, // 1 hour ago (within default 24h) +   ~~~~~~~~~~ +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792065998377} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792152399463, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792065998377} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792152399463, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "ba669e8f"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +PASS src/services/historical-backfill-service.test.ts +ts-jest[ts-compiler] (WARN) src/services/retry-policy-integration.test.ts:631:7 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +631 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:658:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +658 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:680:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +680 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:702:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +702 baseDelayMs: 10, +   ~~~~~~~~~~~ +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +ts-jest[ts-compiler] (WARN) src/services/webhook-verifier.test.ts:537:11 - +error TS7022: 'outcome' implicitly has type 'any' because it does not have a type annotation and is +referenced directly or indirectly in its own initializer. + +537 const outcome = verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:539:5 - error TS2353: Object +literal may only specify known properties, and 'const' does not exist in type 'WebhookVerificationContext'. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~~~ +src/services/webhook-verifier.test.ts:539:11 - error TS2588: Cannot +assign to 'sig' because it is a constant. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~ +src/services/webhook-verifier.test.ts:540:11 - error TS2448: Block-scoped +variable 'outcome' used before its declaration. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ + + src/services/webhook-verifier.test.ts:537:11 + 537 const outcome = verifyWebhookRequest({ +    ~~~~~~~ + 'outcome' is declared here. +src/services/webhook-verifier.test.ts:540:11 - error TS2588: Cannot +assign to 'outcome' because it is a constant. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:550:12 - error TS7006: Parameter +'outcome' implicitly has an 'any' type. + +550 expect(outcome.authenticated).toBe(false); +   ~~~~~~~ +src/services/webhook-verifier.test.ts:550:35 - error TS2339: Property +'toBe' does not exist on type 'Promise'. + +550 expect(outcome.authenticated).toBe(false); +   ~~~~ +src/services/webhook-verifier.test.ts:604:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +604 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:604:20 + 604 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:605:20 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +605 expect(outcome.statusCode).toBe(401); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:605:20 + 605 expect(outcome.statusCode).toBe(401); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:606:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +606 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:606:20 + 606 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:616:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +616 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:616:20 + 616 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:631:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +631 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:631:20 + 631 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:642:21 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +642 expect(replay.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:642:21 + 642 expect(replay.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:643:21 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +643 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:643:21 + 643 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:660:18 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +660 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:660:18 + 660 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:671:19 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +671 expect(second.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:671:19 + 671 expect(second.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:672:19 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +672 expect(second.statusCode).toBe(409); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:672:19 + 672 expect(second.statusCode).toBe(409); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:673:19 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +673 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:673:19 + 673 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:698:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +698 expect(a.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:698:14 + 698 expect(a.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:699:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +699 expect(b.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:699:14 + 699 expect(b.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:712:16 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +712 expect(bad.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:712:16 + 712 expect(bad.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:721:17 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +721 expect(good.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:721:17 + 721 expect(good.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:732:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +732 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:732:20 + 732 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:733:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +733 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:733:20 + 733 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:743:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +743 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:743:20 + 743 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:744:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +744 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:744:20 + 744 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:759:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +759 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:759:20 + 759 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:773:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +773 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:773:20 + 773 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:539:11 - error TS1005: ':' expected. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~ +src/services/webhook-verifier.test.ts:539:68 - error TS1005: ',' expected. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~ +src/services/webhook-verifier.test.ts:540:11 - error TS1005: ':' expected. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:549:7 - error TS1005: ',' expected. + +549 }); +   ~ +src/services/webhook-verifier.test.ts:550:19 - error TS1005: ',' expected. + +550 expect(outcome.authenticated).toBe(false); +   ~ +src/services/webhook-verifier.test.ts:550:33 - error TS1005: ',' expected. + +550 expect(outcome.authenticated).toBe(false); +   ~ +FAIL src/services/webhook-verifier.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\services\webhook-verifier.test.ts:458 + expect(outcome) { }, : .authenticated + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +PASS src/tests/notification-scheduler.test.ts + ● Console + + console.log + 2026-10-16T12:06:42.437Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.924Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.926Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.928Z info: Database initialized successfully +{"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.946Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.961Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.964Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.966Z info: Processing scheduled notification +{"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:42.967Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":1029} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:42.987Z info: Notification marked as completed {"requestId":"test-request-id","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:42.994Z info: Notification delivered successfully +{"requestId":"test-request-id","id":1,"type":"webhook","durationMs":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:42.995Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-success-test","count":1,"durationMs":44} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:42.997Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.037Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.048Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.049Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.051Z info: Processing scheduled notification +{"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.052Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":1024} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.054Z error: Failed to process notification +{"requestId":"test-request-id","id":2,"error":{"message":"secret +token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.062Z error: Job monitor: job failed +{"jobId":"notification-2","jobName":"scheduled-notification","error":"secret token=should-not-persist","durationMs":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.076Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:06:48.066Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.086Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":48} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.088Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.124Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.152Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.161Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.177Z info: Expired notification skipped before delivery +{"requestId":"test-request-id","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.226Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.235Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.236Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.237Z info: Processing scheduled notification +{"requestId":"test-request-id","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.238Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":5,"executeAt":{},"now":{},"missedByMs":60023} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.239Z error: Failed to process notification +{"requestId":"test-request-id","id":5,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary +outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.239Z error: Job monitor: job failed +{"jobId":"notification-5","jobName":"scheduled-notification","error":"temporary outage","durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.249Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:06:43.340Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.258Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":31} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.279Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.280Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-expiration-test","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.295Z info: Expired retry skipped before delivery +{"requestId":"test-request-id","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.296Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.356Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.365Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.375Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.376Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.376Z info: Processing scheduled notification +{"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.377Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1056} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.386Z info: Notification marked as completed {"requestId":"test-request-id","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.393Z info: Notification delivered successfully +{"requestId":"test-request-id","id":6,"type":"discord","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.394Z info: Processing scheduled notification +{"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.395Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":1038} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.404Z info: Notification marked as completed {"requestId":"test-request-id","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.411Z info: Notification delivered successfully +{"requestId":"test-request-id","id":7,"type":"discord","durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.412Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":47} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.413Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.446Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.455Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.464Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.491Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.504Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.505Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.506Z info: Processing scheduled notification +{"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.506Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60024} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.507Z error: Failed to process notification +{"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: +temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.507Z error: Job monitor: job failed +{"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.519Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:06:45.508Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:06:43.528Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":36} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.547Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.548Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.548Z info: Retrying notification +{"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.559Z info: Notification marked as completed {"requestId":"test-request-id","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.566Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.569Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.601Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.611Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.637Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.648Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.671Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.702Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.714Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.738Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.747Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.785Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.802Z warn: Notification moved to dead letter queue +{"id":15,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.803Z info: Notification marked for retry or failed +{"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.834Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.846Z info: Notification cancelled {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.873Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.881Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.910Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:43.919Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.320Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.414Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.583Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.593Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.612Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.621Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.650Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.660Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.662Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:44.664Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.152Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.153Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.153Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.197Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.219Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.248Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.262Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.287Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.313Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.314Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.340Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.352Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-stale"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.376Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.410Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.424Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.426Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +PASS src/services/retry-policy.test.ts +ts-jest[ts-compiler] (WARN) src/__tests__/stress.test.ts:630:52 - error +TS2339: Property 'totalSent' does not exist on type '{ timeoutCount: number; acceptedRequests: number; +skippedDuplicates: number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: +number; hitRatio: number; totalChecks: number; }'. + +630 console.log(` Total Sent: ${metrics.totalSent}`); +   ~~~~~~~~~ +src/__tests__/stress.test.ts:631:52 - error TS2339: Property +'duplicatesSkipped' does not exist on type '{ timeoutCount: number; acceptedRequests: number; skippedDuplicates: +number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: number; hitRatio: +number; totalChecks: number; }'. + +631 console.log(` Duplicates Skipped: ${metrics.duplicatesSkipped}`); +   ~~~~~~~~~~~~~~~~~ +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (28.714 s) + ● Console + + console.log + 2026-10-16T12:06:39.707Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.727Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.729Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.731Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.790Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.791Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.793Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.794Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.986Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.988Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.989Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:39.991Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.160Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.191Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.192Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.195Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.941Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.944Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.945Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:45.946Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:46.020Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:46.021Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:46.022Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:46.024Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.117Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.118Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.120Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.120Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.147Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.148Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.150Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:54.151Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/tests/template-api-integration.test.ts + ● Console + + console.log + 2026-10-16T12:06:54.947Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.417Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.419Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.423Z info: Database initialized successfully {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.425Z error: Database run error {"sql":"duplicate inserts with the same key are +silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.427Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue +(\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n +notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n +target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details +TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count +INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES +scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.429Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS +update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n +UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.429Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.430Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE +notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.430Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.431Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.432Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.432Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.433Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.444Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.445Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.446Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.447Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.448Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.450Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.452Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.453Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.454Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.455Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.456Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.457Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.458Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.459Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.459Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.460Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.461Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.462Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.462Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.463Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.464Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.465Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.466Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.467Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.469Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.470Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.471Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.472Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.477Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.478Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.479Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.480Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.481Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:55.483Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/retry-deduplication.test.ts (10.592 s) + ● Console + + console.log + 2026-10-16T12:06:56.112Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.590Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.591Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.594Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.603Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.622Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.645Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.646Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.670Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Final failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.671Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.674Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:56.777Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.228Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.229Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.229Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.238Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.254Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.256Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.368Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.955Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.955Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.956Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.965Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:57.986Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.011Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Second attempt failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.012Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.030Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.033Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.144Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.990Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.991Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:58.993Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.005Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.025Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.040Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.061Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.101Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.112Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.136Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.158Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.180Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.191Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.212Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.231Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.263Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.264Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.290Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.291Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.301Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.321Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.330Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.347Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.445Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.454Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.470Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.484Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.499Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.508Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.526Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.542Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.566Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.567Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.603Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.604Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.614Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.632Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.643Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.660Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.677Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.686Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.700Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.716Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.733Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.743Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.758Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.774Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.796Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.797Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.824Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.825Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.833Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.854Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.862Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.877Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.893Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.902Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.916Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.934Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.951Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.960Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.977Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:06:59.994Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.023Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.024Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.052Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.052Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.061Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.078Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.089Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.104Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.120Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.129Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.146Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.164Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.180Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.189Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.203Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.219Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.239Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.240Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.264Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.265Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.275Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.292Z info: Notification marked as completed {"id":21} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.302Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.319Z info: Notification marked for retry or failed +{"id":22,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.338Z info: Notification marked as completed {"id":22} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.346Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.361Z info: Notification marked for retry or failed +{"id":23,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.378Z info: Notification marked for retry or failed +{"id":23,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.392Z info: Notification marked as completed {"id":23} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.401Z info: Scheduled notification created {"id":24,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.418Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.432Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.458Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.459Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.481Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.481Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.490Z info: Scheduled notification created {"id":25,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.508Z info: Notification marked as completed {"id":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.518Z info: Scheduled notification created {"id":26,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.533Z info: Notification marked for retry or failed +{"id":26,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.549Z info: Notification marked as completed {"id":26} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.559Z info: Scheduled notification created {"id":27,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.575Z info: Notification marked for retry or failed +{"id":27,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.593Z info: Notification marked for retry or failed +{"id":27,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.609Z info: Notification marked as completed {"id":27} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.618Z info: Scheduled notification created {"id":28,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.635Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.651Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.677Z warn: Notification moved to dead letter queue +{"id":28,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.677Z info: Notification marked for retry or failed +{"id":28,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.705Z warn: Notification moved to dead letter queue +{"id":28,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.705Z info: Notification marked for retry or failed +{"id":28,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.717Z info: Scheduled notification created {"id":29,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.733Z info: Notification marked as completed {"id":29} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.742Z info: Scheduled notification created {"id":30,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.780Z info: Notification marked for retry or failed +{"id":30,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.827Z info: Notification marked as completed {"id":30} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.836Z info: Scheduled notification created {"id":31,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.854Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.871Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.886Z info: Notification marked as completed {"id":31} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.895Z info: Scheduled notification created {"id":32,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.911Z info: Notification marked for retry or failed +{"id":32,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.926Z info: Notification marked for retry or failed +{"id":32,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.950Z warn: Notification moved to dead letter queue +{"id":32,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.951Z info: Notification marked for retry or failed +{"id":32,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.974Z warn: Notification moved to dead letter queue +{"id":32,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.975Z info: Notification marked for retry or failed +{"id":32,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:00.984Z info: Scheduled notification created {"id":33,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.000Z info: Notification marked as completed {"id":33} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.010Z info: Scheduled notification created {"id":34,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.027Z info: Notification marked for retry or failed +{"id":34,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.044Z info: Notification marked as completed {"id":34} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.053Z info: Scheduled notification created {"id":35,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.070Z info: Notification marked for retry or failed +{"id":35,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.084Z info: Notification marked for retry or failed +{"id":35,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.099Z info: Notification marked as completed {"id":35} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.109Z info: Scheduled notification created {"id":36,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.126Z info: Notification marked for retry or failed +{"id":36,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.141Z info: Notification marked for retry or failed +{"id":36,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.165Z warn: Notification moved to dead letter queue +{"id":36,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.167Z info: Notification marked for retry or failed +{"id":36,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.189Z warn: Notification moved to dead letter queue +{"id":36,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.190Z info: Notification marked for retry or failed +{"id":36,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.199Z info: Scheduled notification created {"id":37,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.217Z info: Notification marked as completed {"id":37} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.226Z info: Scheduled notification created {"id":38,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.242Z info: Notification marked for retry or failed +{"id":38,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.259Z info: Notification marked as completed {"id":38} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.269Z info: Scheduled notification created {"id":39,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.284Z info: Notification marked for retry or failed +{"id":39,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.302Z info: Notification marked for retry or failed +{"id":39,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.317Z info: Notification marked as completed {"id":39} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.326Z info: Scheduled notification created {"id":40,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.341Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.358Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.385Z warn: Notification moved to dead letter queue +{"id":40,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.385Z info: Notification marked for retry or failed +{"id":40,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.411Z warn: Notification moved to dead letter queue +{"id":40,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.412Z info: Notification marked for retry or failed +{"id":40,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.421Z info: Scheduled notification created {"id":41,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.436Z info: Notification marked as completed {"id":41} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.444Z info: Scheduled notification created {"id":42,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.459Z info: Notification marked for retry or failed +{"id":42,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.478Z info: Notification marked as completed {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.487Z info: Scheduled notification created {"id":43,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.501Z info: Notification marked for retry or failed +{"id":43,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.519Z info: Notification marked for retry or failed +{"id":43,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.537Z info: Notification marked as completed {"id":43} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.546Z info: Scheduled notification created {"id":44,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.565Z info: Notification marked for retry or failed +{"id":44,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.584Z info: Notification marked for retry or failed +{"id":44,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.609Z warn: Notification moved to dead letter queue +{"id":44,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.610Z info: Notification marked for retry or failed +{"id":44,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.633Z warn: Notification moved to dead letter queue +{"id":44,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.634Z info: Notification marked for retry or failed +{"id":44,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.644Z info: Scheduled notification created {"id":45,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.661Z info: Notification marked as completed {"id":45} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.672Z info: Scheduled notification created {"id":46,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.687Z info: Notification marked for retry or failed +{"id":46,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.703Z info: Notification marked as completed {"id":46} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.712Z info: Scheduled notification created {"id":47,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.729Z info: Notification marked for retry or failed +{"id":47,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.746Z info: Notification marked for retry or failed +{"id":47,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.761Z info: Notification marked as completed {"id":47} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.771Z info: Scheduled notification created {"id":48,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.787Z info: Notification marked for retry or failed +{"id":48,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.802Z info: Notification marked for retry or failed +{"id":48,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.826Z warn: Notification moved to dead letter queue +{"id":48,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.827Z info: Notification marked for retry or failed +{"id":48,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.850Z warn: Notification moved to dead letter queue +{"id":48,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.852Z info: Notification marked for retry or failed +{"id":48,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.861Z info: Scheduled notification created {"id":49,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.879Z info: Notification marked as completed {"id":49} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.888Z info: Scheduled notification created {"id":50,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.905Z info: Notification marked for retry or failed +{"id":50,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.922Z info: Notification marked as completed {"id":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.931Z info: Scheduled notification created {"id":51,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.949Z info: Notification marked for retry or failed +{"id":51,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.965Z info: Notification marked for retry or failed +{"id":51,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.984Z info: Notification marked as completed {"id":51} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:01.994Z info: Scheduled notification created {"id":52,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.010Z info: Notification marked for retry or failed +{"id":52,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.027Z info: Notification marked for retry or failed +{"id":52,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.052Z warn: Notification moved to dead letter queue +{"id":52,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.053Z info: Notification marked for retry or failed +{"id":52,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.083Z warn: Notification moved to dead letter queue +{"id":52,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.085Z info: Notification marked for retry or failed +{"id":52,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.095Z info: Scheduled notification created {"id":53,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.114Z info: Notification marked as completed {"id":53} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.124Z info: Scheduled notification created {"id":54,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.139Z info: Notification marked for retry or failed +{"id":54,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.156Z info: Notification marked as completed {"id":54} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.182Z info: Scheduled notification created {"id":55,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.201Z info: Notification marked for retry or failed +{"id":55,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.223Z info: Notification marked for retry or failed +{"id":55,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.243Z info: Notification marked as completed {"id":55} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.254Z info: Scheduled notification created {"id":56,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.276Z info: Notification marked for retry or failed +{"id":56,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.298Z info: Notification marked for retry or failed +{"id":56,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.327Z warn: Notification moved to dead letter queue +{"id":56,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.328Z info: Notification marked for retry or failed +{"id":56,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.348Z warn: Notification moved to dead letter queue +{"id":56,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.350Z info: Notification marked for retry or failed +{"id":56,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.359Z info: Scheduled notification created {"id":57,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.377Z info: Notification marked as completed {"id":57} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.386Z info: Scheduled notification created {"id":58,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.402Z info: Notification marked for retry or failed +{"id":58,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.419Z info: Notification marked as completed {"id":58} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.428Z info: Scheduled notification created {"id":59,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.443Z info: Notification marked for retry or failed +{"id":59,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.460Z info: Notification marked for retry or failed +{"id":59,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.477Z info: Notification marked as completed {"id":59} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.486Z info: Scheduled notification created {"id":60,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.503Z info: Notification marked for retry or failed +{"id":60,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.519Z info: Notification marked for retry or failed +{"id":60,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.543Z warn: Notification moved to dead letter queue +{"id":60,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.544Z info: Notification marked for retry or failed +{"id":60,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.571Z warn: Notification moved to dead letter queue +{"id":60,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.572Z info: Notification marked for retry or failed +{"id":60,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.582Z info: Scheduled notification created {"id":61,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.607Z info: Notification marked as completed {"id":61} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.618Z info: Scheduled notification created {"id":62,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.634Z info: Notification marked for retry or failed +{"id":62,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.655Z info: Notification marked as completed {"id":62} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.665Z info: Scheduled notification created {"id":63,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.685Z info: Notification marked for retry or failed +{"id":63,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.705Z info: Notification marked for retry or failed +{"id":63,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.726Z info: Notification marked as completed {"id":63} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.741Z info: Scheduled notification created {"id":64,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.759Z info: Notification marked for retry or failed +{"id":64,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.776Z info: Notification marked for retry or failed +{"id":64,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.821Z warn: Notification moved to dead letter queue +{"id":64,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.821Z info: Notification marked for retry or failed +{"id":64,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.844Z warn: Notification moved to dead letter queue +{"id":64,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.845Z info: Notification marked for retry or failed +{"id":64,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.854Z info: Scheduled notification created {"id":65,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.870Z info: Notification marked as completed {"id":65} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.879Z info: Scheduled notification created {"id":66,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.897Z info: Notification marked for retry or failed +{"id":66,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.912Z info: Notification marked as completed {"id":66} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.921Z info: Scheduled notification created {"id":67,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.937Z info: Notification marked for retry or failed +{"id":67,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.952Z info: Notification marked for retry or failed +{"id":67,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.970Z info: Notification marked as completed {"id":67} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.979Z info: Scheduled notification created {"id":68,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:02.993Z info: Notification marked for retry or failed +{"id":68,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.009Z info: Notification marked for retry or failed +{"id":68,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.033Z warn: Notification moved to dead letter queue +{"id":68,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.034Z info: Notification marked for retry or failed +{"id":68,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.057Z warn: Notification moved to dead letter queue +{"id":68,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.058Z info: Notification marked for retry or failed +{"id":68,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.065Z info: Scheduled notification created {"id":69,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.081Z info: Notification marked as completed {"id":69} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.090Z info: Scheduled notification created {"id":70,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.106Z info: Notification marked for retry or failed +{"id":70,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.121Z info: Notification marked as completed {"id":70} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.129Z info: Scheduled notification created {"id":71,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.146Z info: Notification marked for retry or failed +{"id":71,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.161Z info: Notification marked for retry or failed +{"id":71,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.178Z info: Notification marked as completed {"id":71} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.186Z info: Scheduled notification created {"id":72,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.201Z info: Notification marked for retry or failed +{"id":72,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.218Z info: Notification marked for retry or failed +{"id":72,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.242Z warn: Notification moved to dead letter queue +{"id":72,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.242Z info: Notification marked for retry or failed +{"id":72,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.267Z warn: Notification moved to dead letter queue +{"id":72,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.268Z info: Notification marked for retry or failed +{"id":72,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.277Z info: Scheduled notification created {"id":73,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.292Z info: Notification marked as completed {"id":73} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.301Z info: Scheduled notification created {"id":74,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.320Z info: Notification marked for retry or failed +{"id":74,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.335Z info: Notification marked as completed {"id":74} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.343Z info: Scheduled notification created {"id":75,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.360Z info: Notification marked for retry or failed +{"id":75,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.375Z info: Notification marked for retry or failed +{"id":75,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.391Z info: Notification marked as completed {"id":75} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.399Z info: Scheduled notification created {"id":76,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.415Z info: Notification marked for retry or failed +{"id":76,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.431Z info: Notification marked for retry or failed +{"id":76,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.453Z warn: Notification moved to dead letter queue +{"id":76,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.454Z info: Notification marked for retry or failed +{"id":76,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.479Z warn: Notification moved to dead letter queue +{"id":76,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.480Z info: Notification marked for retry or failed +{"id":76,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.489Z info: Scheduled notification created {"id":77,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.504Z info: Notification marked as completed {"id":77} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.513Z info: Scheduled notification created {"id":78,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.527Z info: Notification marked for retry or failed +{"id":78,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.543Z info: Notification marked as completed {"id":78} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.552Z info: Scheduled notification created {"id":79,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.569Z info: Notification marked for retry or failed +{"id":79,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.584Z info: Notification marked for retry or failed +{"id":79,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.602Z info: Notification marked as completed {"id":79} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.610Z info: Scheduled notification created {"id":80,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.626Z info: Notification marked for retry or failed +{"id":80,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.645Z info: Notification marked for retry or failed +{"id":80,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.667Z warn: Notification moved to dead letter queue +{"id":80,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.669Z info: Notification marked for retry or failed +{"id":80,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.694Z warn: Notification moved to dead letter queue +{"id":80,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.695Z info: Notification marked for retry or failed +{"id":80,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.704Z info: Scheduled notification created {"id":81,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.721Z info: Notification marked as completed {"id":81} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.730Z info: Scheduled notification created {"id":82,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.745Z info: Notification marked for retry or failed +{"id":82,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.762Z info: Notification marked as completed {"id":82} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.771Z info: Scheduled notification created {"id":83,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.787Z info: Notification marked for retry or failed +{"id":83,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.803Z info: Notification marked for retry or failed +{"id":83,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.821Z info: Notification marked as completed {"id":83} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.829Z info: Scheduled notification created {"id":84,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.844Z info: Notification marked for retry or failed +{"id":84,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.862Z info: Notification marked for retry or failed +{"id":84,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.884Z warn: Notification moved to dead letter queue +{"id":84,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.885Z info: Notification marked for retry or failed +{"id":84,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.911Z warn: Notification moved to dead letter queue +{"id":84,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.912Z info: Notification marked for retry or failed +{"id":84,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.922Z info: Scheduled notification created {"id":85,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.939Z info: Notification marked as completed {"id":85} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.948Z info: Scheduled notification created {"id":86,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.963Z info: Notification marked for retry or failed +{"id":86,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.980Z info: Notification marked as completed {"id":86} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:03.989Z info: Scheduled notification created {"id":87,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.006Z info: Notification marked for retry or failed +{"id":87,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.006Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.007Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[87,2,"RETRY","Attempt 2 +failed",null,700],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.008Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.123Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.124Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.124Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.126Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[87,2,"RETRY","Attempt 2 +failed",null,700],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.582Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.583Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.584Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.595Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.621Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.630Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.642Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.670Z info: Fetched and locked pending notifications +{"count":1,"processorId":"test-processor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.672Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.790Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.797Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.797Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.798Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.808Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.823Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.831Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.841Z info: Notification cancelled {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.843Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.947Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.954Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.955Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.955Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.964Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.975Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:04.977Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.086Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.092Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.093Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.093Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.101Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.110Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.138Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.160Z info: Notification marked as completed {"id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.162Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.271Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.277Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.278Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.278Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.287Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.303Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.318Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.334Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":3,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.350Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":4,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.368Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":5,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.384Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":6,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.400Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":7,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.415Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":8,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.440Z warn: Notification moved to dead letter queue +{"id":9,"notificationType":"discord","failureReason":"Attempt 9 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.441Z info: Notification marked for retry or failed +{"id":9,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.457Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.459Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.570Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.576Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.577Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.577Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.586Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.601Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.610Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.626Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.637Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.653Z info: Notification marked as completed {"id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.663Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.678Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.686Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.703Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.712Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.728Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.745Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.754Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.769Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.786Z info: Notification marked as completed {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.795Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.812Z info: Notification marked for retry or failed +{"id":17,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.828Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.851Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.868Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.884Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.901Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.911Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.929Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.944Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.966Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.977Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:05.993Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:06.011Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:06.036Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:06.037Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:06.061Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:06.062Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:06.063Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +PASS src/services/retry-scheduler-webhook.test.ts +ts-jest[ts-compiler] (WARN) src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18 - +error TS2339: Property 'clear' does not exist on type 'NotificationDeduplicator'. + +56 deduplicator.clear(); +   ~~~~~ +src/__tests__/smoke/notification-pipeline.smoke.test.ts:602:5 - error TS2322: +Type 'string' is not assignable to type 'Contract'. + +602 contractId: contractAddress, +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Console + + console.log + 2026-10-16T12:07:08.253Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:08.258Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:08.258Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:08.259Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:08.260Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event from ingestion to notification +generation + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should handle multiple events in sequence + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing id + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with invalid ledger + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching wildcard filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching specific filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should reject event not matching filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should extract event name from topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should prevent duplicate notifications + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should allow same event from different contracts + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should handle notification failure gracefully + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should continue processing after individual event failure + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should process events quickly (< 100ms +per event) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should handle registry size limits + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should clean up resources properly + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should execute in under 2 seconds + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should make zero external network calls + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should run deterministically (same input = same +output) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/discord-notification.test.ts (5.614 s) + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +ts-jest[ts-compiler] (WARN) src/services/notification-expiration.test.ts:40:9 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +40 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:45:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +45 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:46:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +46 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:63:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +63 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:68:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +68 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:69:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +69 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:86:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +86 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:91:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +91 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:92:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +92 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:116:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +116 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:121:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +121 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:122:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +122 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:140:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +140 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:145:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +145 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:146:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +146 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:164:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +164 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:169:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +169 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:170:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +170 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:199:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +199 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:204:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +204 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:205:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +205 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:222:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +222 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:227:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +227 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:228:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +228 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:340:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +340 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:345:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +345 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:346:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +346 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:379:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +379 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:384:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +384 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:385:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +385 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:407:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +407 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:412:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +412 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:413:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +413 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:435:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +435 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:440:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +440 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:441:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +441 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:467:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +467 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:472:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +472 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:473:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +473 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:503:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +503 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:508:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +508 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:509:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +509 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:521:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +521 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:526:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +526 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:527:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +527 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:552:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +552 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:557:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +557 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:558:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +558 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +PASS src/services/event-subscriber-checkpoint.test.ts +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +PASS src/services/event-processing-queue.test.ts +PASS src/services/dns-rebinding-guard.test.ts +ts-jest[ts-compiler] (WARN) src/services/archive.test.ts:237:7 - error +TS2741: Property 'expiresAt' is missing in type '{ originalId: number; payload: string; notificationType: string; +targetRecipient: string; executeAt: string; createdAt: string; processingCompletedAt: string; status: string; +retryCount: number; lastError: null; eventId: null; contractAddress: null; metadata: null; }' but required in type '{ +originalId: number; payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: +string | null; createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +237 { +   ~ +238 originalId: 1, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +250 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +251 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +src/services/archive.test.ts:252:7 - error TS2741: Property 'expiresAt' +is missing in type '{ originalId: number; payload: string; notificationType: string; targetRecipient: string; +executeAt: string; createdAt: string; processingCompletedAt: null; status: string; retryCount: number; lastError: +string; eventId: string; contractAddress: string; metadata: null; }' but required in type '{ originalId: number; +payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: string | null; +createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +252 { +   ~ +253 originalId: 2, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +265 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +266 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +FAIL src/services/archive.test.ts + ● Console + + console.log + 2026-10-16T12:07:17.154Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T1 +2:07:17.154Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.161Z info: ArchiveService: cycle complete +{"archived":2,"purged":0,"durationMs":7,"jobId":"archive-cycle-1792152437153"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.162Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.164Z info: ArchiveService: archived processed notification immediately {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.165Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":0,"jobId":"archive-cycle-1792152437165"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.166Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.167Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":0,"jobId":"archive-cycle-1792152437167"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.168Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.170Z info: ArchiveService: purged expired archive records +{"count":2,"cutoff":"2026-07-18T12:07:17.170Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.171Z info: ArchiveService: cycle complete +{"archived":0,"purged":2,"durationMs":1,"jobId":"archive-cycle-1792152437170"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.171Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.172Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":0,"jobId":"archive-cycle-1792152437172"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.173Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.173Z info: ArchiveService started +{"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.173Z info: Waiting for active archival jobs to complete {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.173Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.176Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":3,"jobId":"archive-cycle-1792152437173"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.177Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.178Z info: Handling GET /api/archive {"requestId":"req-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.185Z error: Failed to query archive {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n at sendOk +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n +at handleArchiveRequest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:116:13)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\services\\archive.test.ts:460:21)"},"requestId":"req-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.186Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.187Z error: Failed to fetch archive record {"error":{"message":"res.setHeader is +not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n +at sendErr (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\response.ts:155:3)\n at handleArchiveRequest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:76:16)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\archive.test.ts:471:21)"},"requestId":"req-3","id":999} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.188Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.189Z error: Failed to fetch archive record {"error":{"message":"res.setHeader is +not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n +at sendOk (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n at handleArchiveRequest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:79:13)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\archive.test.ts:497:21)"},"requestId":"req-4","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.191Z info: Handling POST /api/archive/run {"requestId":"req-6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:17.191Z error: Archive run failed {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n at sendOk +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n +at handleArchiveRequest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:55:13)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\services\\archive.test.ts:517:21)"},"requestId":"req-6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● handleArchiveRequest › GET /api/archive returns paginated results + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:125:14) + at Object. (src/services/archive.test.ts:460:21) + + ● handleArchiveRequest › GET /api/archive/:id returns 404 for unknown id + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:82:14) + at Object. (src/services/archive.test.ts:471:21) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:82:14) + at Object. (src/services/archive.test.ts:497:21) + + ● handleArchiveRequest › POST /api/archive/run returns 503 when service not provided + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:49:14) + at Object. (src/services/archive.test.ts:506:47) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:58:14) + at Object. (src/services/archive.test.ts:517:21) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › classifies an AbortError as a timeout + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:431:36) + + ● sendWebhookWithRetry › classifyWebhookFailure › classifies a TimeoutError as a timeout + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:438:36) + + ● sendWebhookWithRetry › classifyWebhookFailure › classifies a generic error as a network failure (not a timeout) + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:442:36) + + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:448:36) + +PASS src/services/stellar-rpc-manager.test.ts +PASS src/api/rate-limiter.test.ts +PASS src/api/rate-limit-scenarios.test.ts +ts-jest[ts-compiler] (WARN) src/api/__tests__/api-contract.test.ts:20:46 - +error TS2345: Argument of type '{ status: string; }' is not assignable to parameter of type 'never'. + +20 getHealth: jest.fn().mockResolvedValue({ status: 'healthy' }), +   ~~~~~~~~~~~~~~~~~~~~~ +src/api/__tests__/api-contract.test.ts:60:38 - error TS2345: Argument of +type '{ ok: number; }' is not assignable to parameter of type 'never'. + +60 get: jest.fn().mockResolvedValue({ ok: 1 }), +   ~~~~~~~~~ +PASS src/api/__tests__/api-contract.test.ts +ts-jest[ts-compiler] (WARN) +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:164:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +164 { baseDelayMs: 1000, maxRetries: 3, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:203:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +203 { baseDelayMs: 1000, maxRetries: 1, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (10.9 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +ts-jest[ts-compiler] (WARN) src/tests/notification-scheduler-refactored.test.ts:309:13 - +error TS2451: Cannot redeclare block-scoped variable 'pastLock'. + +309 const pastLock = NotificationFixtureBuilder.dates.past(1000); +   ~~~~~~~~ +src/tests/notification-scheduler-refactored.test.ts:334:13 - error TS2451: +Cannot redeclare block-scoped variable 'pastLock'. + +334 const pastLock = NotificationFixtureBuilder.dates.past(1000); +   ~~~~~~~~ +FAIL src/tests/notification-scheduler-refactored.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\tests\notification-scheduler-refactored.test.ts:271 + const pastLock = notification_fixture_builder_1.NotificationFixtureBuilder.dates.past(1000); + ^ + + SyntaxError: Identifier 'pastLock' has already been declared + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +PASS src/migrations/004-database-data-integrity.test.ts + ● Console + + console.log + 2026-10-16T12:07:40.571Z info: Migration 004 (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.605Z info: Migration 004-repeat (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.620Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 +aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 +row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data +first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.624Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.633Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.637Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.638Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.640Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', +'2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:07:40.641Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/discord-notification-refactored.test.ts (77.206 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/api/queue-visibility.test.ts (33.364 s) + ● Console + + console.log + 2026-10-16T12:09:04.131Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.045Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.074Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.150Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.248Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.388Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.441Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.607Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:05.692Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:06.998Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:07.012Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:07.026Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:07.103Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:07.134Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:08.405Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:08.410Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:08.412Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:08.449Z info: Handling GET /api/schedule/queue +{"requestId":"b74130d0","correlationId":"926791d9-ec55-4309-8a17-15d5146e0a0e","count":0,"durationMs":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:08.640Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:08.682Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.363Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.366Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.378Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.409Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.427Z info: Handling GET /api/schedule/queue +{"requestId":"74a8a73e","correlationId":"9b5fb648-2c5c-4761-badf-ca0cc289373c","count":1,"durationMs":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.450Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:10.467Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.312Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.317Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.321Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.346Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.433Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.467Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.683Z warn: Notification moved to dead letter queue +{"id":2,"notificationType":"discord","failureReason":"oops"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.732Z info: Notification marked for retry or failed +{"id":2,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.858Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.915Z info: Handling GET /api/schedule/queue +{"requestId":"3be7244e","correlationId":"42768640-a1db-4a64-9621-3938ae118b93","count":1,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.938Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:11.999Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:12.881Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:12.883Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:12.884Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:12.911Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.012Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.131Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.212Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.283Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.343Z info: Handling GET /api/schedule/queue +{"requestId":"f86dc6f7","correlationId":"46bded51-3941-427a-944e-2fdabc32594a","count":3,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.349Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:13.497Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:14.569Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:14.583Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:14.593Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:14.769Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:14.924Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.063Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.067Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.068Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.179Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.223Z info: Handling GET /api/schedule/queue +{"requestId":"d06a239c","correlationId":"0e621ab6-0be0-44be-9f49-491f4de80376","count":1,"durationMs":34} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.235Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:16.312Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.403Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.733Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.749Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.764Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.799Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.816Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:21.828Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:26.429Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:26.495Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:26.499Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:26.508Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + Server is not running. + +PASS src/services/event-deduplication-service.test.ts +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +ts-jest[ts-compiler] (WARN) src/services/concurrent-event-processing.test.ts:74:40 - +error TS2352: Conversion of type '{ address: string; name: string; }' to type 'ContractConfig' may +be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the +expression to 'unknown' first. + Property 'events' is missing in type '{ address: string; name: string; }' but required in type 'ContractConfig'. + +74 const contractConfig: ContractConfig = { address: CONTRACT, name: 'Concurrency' } as ContractConfig; +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/types/index.ts:18:3 + 18 events: string[]; +    ~~~~~~ + 'events' is declared here. +FAIL src/services/concurrent-event-processing.test.ts (17.795 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 1) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/__tests__/stress.test.ts (193.838 s) + ● Console + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: 1,000 Concurrent Events + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 317ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 3154.57 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 13.13ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 14.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 538.91 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 499.88 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: -39.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 555.49 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 562.16 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 6.66 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: 5,000 Events Sustained Load + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 1130ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 4424.78 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 9.35ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 11.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 502.23 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 536.68 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 34.45 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 562.56 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 508.83 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: -53.73 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: 10,000 Events - System Stability + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 2672ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 3742.51 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 8.50ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 9.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 539.10 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 546.06 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 6.96 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 508.92 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 523.41 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 14.49 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + Sending first burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:355:15) + + console.log + Waiting idle period (2 seconds)... + + at Object. (src/__tests__/stress.test.ts:372:15) + + console.log + Sending second burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:376:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Burst Traffic (2x2000 events) + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 2885ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 1386.48 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 12.68ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 14.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 549.10 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 526.23 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: -22.87 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 523.42 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 526.12 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 2.70 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + First burst time: 442ms + + at Object. (src/__tests__/stress.test.ts:405:15) + + console.log + Second burst time: 436ms + + at Object. (src/__tests__/stress.test.ts:406:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Priority Queue Under Load + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 883ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 2265.01 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 13.83ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 14.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 528.47 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 542.47 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 13.99 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 526.12 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 525.42 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: -0.70 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + Priority Analysis: + + at Object. (src/__tests__/stress.test.ts:491:15) + + console.log + High-priority in first 500: 492 / 500 (98.40%) + + at Object. (src/__tests__/stress.test.ts:492:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Deduplication Test (3000 events, 1500 unique) + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 3000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 1500 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 1332ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 2252.25 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 13.39ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 14.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 544.37 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 553.34 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 8.97 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 525.42 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 525.43 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 0.01 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + Deduplication Efficiency: + + at Object. (src/__tests__/stress.test.ts:562:15) + + console.log + Total Enqueued: 3000 + + at Object. (src/__tests__/stress.test.ts:563:15) + + console.log + Unique Events: 1500 + + at Object. (src/__tests__/stress.test.ts:564:15) + + console.log + Actually Processed: 1500 + + at Object. (src/__tests__/stress.test.ts:565:15) + + console.log + Duplicates Blocked: 1500 + + at Object. (src/__tests__/stress.test.ts:566:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Discord Notification Service Load + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 3077ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 162.50 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 3024.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 3065.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 3047.81ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 3053.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 3064.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 3065.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 555.25 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 346.98 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: -208.27 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 525.43 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 511.79 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: -13.64 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + Discord Service Metrics: + + at Object. (src/__tests__/stress.test.ts:629:15) + + console.log + Total Sent: undefined + + at Object. (src/__tests__/stress.test.ts:630:15) + + console.log + Duplicates Skipped: undefined + + at Object. (src/__tests__/stress.test.ts:631:15) + + ● Stress Test Suite: Notification Processing › 6. Discord Notification Service Under Load › should handle 500 +Discord notifications with deduplication + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: 0 + +   631 | console.log(` Duplicates Skipped: +${metrics.duplicatesSkipped}`); +  632 | + > 633 | +expect(successCount.value).toBe(eventCount); +  | ^ +  634 | expect(report.throughput).toBeGreaterThan(50); +  635 | }, 120000); +  636 | }); + + at Object. (src/__tests__/stress.test.ts:633:34) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   637 | +  638 | describe('7. Error Recovery Under Stress', () => { + > 639 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  640 | const eventCount = 1000; +  641 | const processedIds: string[] = []; +  642 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:639:5 + at src/__tests__/stress.test.ts:638:3 + at Object. (src/__tests__/stress.test.ts:37:1) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + ================================================================================". + +   93 | +  94 | // Log to console for visibility + > 95 | console.log('\n' + +'='.repeat(80)); +  | ^ +  96 | console.log(`STRESS TEST REPORT: ${testName}`); +  97 | console.log('='.repeat(80)); +  98 | console.log(`Events Processed: ${eventCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "STRESS TEST REPORT: Error Recovery Under Load". + +   94 | // Log to console for visibility +  95 | console.log('\n' + +'='.repeat(80)); + > 96 | console.log(`STRESS TEST REPORT: +${testName}`); +  | ^ +  97 | console.log('='.repeat(80)); +  98 | console.log(`Events Processed: ${eventCount}`); +  99 | console.log(`Success Count: ${successCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================". + +   95 | console.log('\n' + +'='.repeat(80)); +  96 | console.log(`STRESS TEST REPORT: ${testName}`); + > 97 | +console.log('='.repeat(80)); +  | ^ +  98 | console.log(`Events Processed: ${eventCount}`); +  99 | console.log(`Success Count: ${successCount}`); +  100 | console.log(`Failure Count: ${failureCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Events Processed: 1000". + +   96 | console.log(`STRESS TEST REPORT: ${testName}`); +  97 | console.log('='.repeat(80)); + > 98 | console.log(`Events Processed: +${eventCount}`); +  | ^ +  99 | console.log(`Success Count: ${successCount}`); +  100 | console.log(`Failure Count: ${failureCount}`); +  101 | console.log(`Total Time: ${totalTimeMs}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Success Count: 900". + +   97 | console.log('='.repeat(80)); +  98 | console.log(`Events Processed: ${eventCount}`); + > 99 | console.log(`Success Count: +${successCount}`); +  | ^ +  100 | console.log(`Failure Count: ${failureCount}`); +  101 | console.log(`Total Time: ${totalTimeMs}ms`); +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Failure Count: 0". + +   98 | console.log(`Events Processed: ${eventCount}`); +  99 | console.log(`Success Count: ${successCount}`); + > 100 | console.log(`Failure Count: +${failureCount}`); +  | ^ +  101 | console.log(`Total Time: ${totalTimeMs}ms`); +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  103 | console.log(`\nLatency Statistics:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Total Time: 181279ms". + +   99 | console.log(`Success Count: ${successCount}`); +  100 | console.log(`Failure Count: ${failureCount}`); + > 101 | console.log(`Total Time: +${totalTimeMs}ms`); +  | ^ +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  103 | console.log(`\nLatency Statistics:`); +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Throughput: 5.52 events/second". + +   100 | console.log(`Failure Count: ${failureCount}`); +  101 | console.log(`Total Time: ${totalTimeMs}ms`); + > 102 | console.log(`Throughput: +${throughput.toFixed(2)} events/second`); +  | ^ +  103 | console.log(`\nLatency Statistics:`); +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Latency Statistics:". + +   101 | console.log(`Total Time: ${totalTimeMs}ms`); +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + > 103 | console.log(`\nLatency Statistics:`); +  | ^ +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Min: 1.00ms". + +   102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  103 | console.log(`\nLatency Statistics:`); + > 104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  | ^ +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Max: 18.00ms". + +   103 | console.log(`\nLatency Statistics:`); +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + > 105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  | ^ +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Avg: 13.77ms". + +   104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + > 106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  | ^ +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); +  109 | console.log(` P99: ${p99.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P50 (Median): 14.00ms". + +   105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + > 107 | console.log(` P50 (Median): +${p50.toFixed(2)}ms`); +  | ^ +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); +  109 | console.log(` P99: ${p99.toFixed(2)}ms`); +  110 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P95: 16.00ms". + +   106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + > 108 | console.log(` P95: +${p95.toFixed(2)}ms`); +  | ^ +  109 | console.log(` P99: ${p99.toFixed(2)}ms`); +  110 | +  111 | if (memoryUsage) { + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P99: 17.00ms". + +   107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); + > 109 | console.log(` P99: +${p99.toFixed(2)}ms`); +  | ^ +  110 | +  111 | if (memoryUsage) { +  112 | console.log(`\nMemory Usage:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Memory Usage:". + +   110 | +  111 | if (memoryUsage) { + > 112 | console.log(`\nMemory Usage:`); +  | ^ +  113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - Heap: 346.73 MB". + +   111 | if (memoryUsage) { +  112 | console.log(`\nMemory Usage:`); + > 113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  | ^ +  114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - Heap: 346.36 MB". + +   112 | console.log(`\nMemory Usage:`); +  113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); + > 114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  | ^ +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - Heap: -0.36 MB". + +   113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); + > 115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  | ^ +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - RSS: 511.79 MB". + +   114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + > 116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  | ^ +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  119 | } + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - RSS: 39.25 MB". + +   115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + > 117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  | ^ +  118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  119 | } +  120 | console.log('='.repeat(80) + +'\n'); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - RSS: -472.55 MB". + +   116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + > 118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  | ^ +  119 | } +  120 | console.log('='.repeat(80) + +'\n'); +  121 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================ + ". + +   118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  119 | } + > 120 | console.log('='.repeat(80) ++ '\n'); +  | ^ +  121 | +  122 | return report; +  123 | }; + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Error Recovery Statistics:". + +   706 | ); +  707 | + > 708 | console.log(`\nError Recovery +Statistics:`); +  | ^ +  709 | console.log(` Initial Failures: ${retryCount}`); +  710 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  711 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:708:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Initial Failures: 300". + +   707 | +  708 | console.log(`\nError Recovery Statistics:`); + > 709 | console.log(` Initial Failures: +${retryCount}`); +  | ^ +  710 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  711 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  712 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:709:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Successful Retries: 200". + +   708 | console.log(`\nError Recovery Statistics:`); +  709 | console.log(` Initial Failures: ${retryCount}`); + > 710 | console.log(` Successful Retries: ${processedIds.length +- (eventCount - retryCount)}`); +  | ^ +  711 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  712 | +  713 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:710:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Final Success Rate: 90.00%". + +   709 | console.log(` Initial Failures: ${retryCount}`); +  710 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); + > 711 | console.log(` Final Success Rate: +${((processedIds.length / eventCount) * 100).toFixed(2)}%`); +  | ^ +  712 | +  713 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success +  714 | expect(retryCount).toBeGreaterThan(0); // Verify +retries occurred + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:711:15) + +FAIL src/services/execution-metrics.test.ts (8.106 s) + ● Console + + console.log + 2026-10-16T12:09:58.142Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:58.883Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:58.887Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:58.900Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:58.926Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:58.992Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.018Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.059Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.081Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.198Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.989Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.992Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:09:59.995Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.012Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.043Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.063Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.092Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.127Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.145Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.176Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.221Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.226Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.272Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.275Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.283Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:00.407Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.231Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.234Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.243Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.263Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.296Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.316Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.349Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.375Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.463Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.494Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.510Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.540Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.573Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.594Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.629Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.663Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.680Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.711Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.758Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.761Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.801Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.808Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.816Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:01.941Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.752Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.777Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.810Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.854Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.890Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.913Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.943Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:02.966Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.030Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.040Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.051Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.179Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.944Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.948Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.950Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:03.962Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:04.084Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.708Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.711Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.714Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.732Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.750Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.783Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:05.793Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +PASS src/utils/redact.test.ts +PASS src/utils/ledger-range-validator.test.ts +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-reorg.test.ts:62:3 - +error TS2353: Object literal may only specify known properties, and 'maxPayloadSizeBytes' does not +exist in type 'Config'. + +62 maxPayloadSizeBytes: 64 * 1024, +   ~~~~~~~~~~~~~~~~~~~ +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "9b0c3a1a"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "b1ef8db2-ed61-45e0-a888-0e572dcd4990", +"eventId": "event-1", "requestId": "b1ef8db2-ed61-45e0-a888-0e572dcd4990"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/api/queue-operational-metrics.test.ts (18.032 s) + ● Console + + console.log + 2026-10-16T12:10:07.757Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152607590-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:08.797Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:08.827Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:08.889Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152607590-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:08.999Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:09.113Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152609048-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.857Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.860Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.861Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152609048-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.885Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.903Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.914Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:10.920Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152610917-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.702Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.707Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.709Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152610917-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.728Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.761Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.779Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:11.795Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152611790-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.728Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.731Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.733Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152611790-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.757Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.776Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.784Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:12.792Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152612787-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.543Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.546Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.547Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152612787-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.566Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.584Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.640Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.642Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.666Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:13.675Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152613673-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:14.625Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:14.627Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:14.631Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152613673-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:14.669Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:14.769Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152614751-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.610Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.613Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.615Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152614751-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.634Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.651Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.668Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.691Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"e8215196","correlationId":"8fccc886-f8c2-4953-8b0b-9b01efc6ec23","durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.716Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:15.724Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152615719-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.767Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.769Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.772Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operat +ional-metrics-1792152615719-8.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database +handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.793Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152620791-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.881Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152615719-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.884Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152615719-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.891Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.893Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:20.896Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152615719-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:21.543Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:21.546Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:21.548Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152620791-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:21.563Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:21.570Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152621567-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.167Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.169Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.173Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152621567-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.192Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.209Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.216Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152623214-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.987Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.991Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:23.993Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792152623214-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:24.023Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:24.035Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:24.051Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:24.060Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + + ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — +Dedicated Endpoint › supports aliases /api/schedule/queue-metrics and /api/notifications/queue-metrics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let testDbPath: string; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | testDbIndex++; +  77 | testDbPath = path.join( +  78 | __dirname, + + at src/api/queue-operational-metrics.test.ts:75:13 + at Object. (src/api/queue-operational-metrics.test.ts:68:9) + +PASS src/migrations/005-notification-expiration.test.ts + ● Console + + console.log + 2026-10-16T12:10:25.812Z info: Migration 005 (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:25.885Z info: Migration 005-repeat (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:25.908Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:25.924Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/test-utils/notification-fixture-builder.test.ts +PASS src/api/events-server.history.test.ts +FAIL src/tests/template-system.test.ts + ● Console + + console.log + 2026-10-16T12:10:30.513Z info: Connected to SQLite database {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:32.135Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:32.141Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:32.148Z info: Database initialized successfully {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:32.154Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX +IF NOT EXISTS idx_templates_unique_key \r\n ON +notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:32.193Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +PASS src/utils/event-utils.test.ts +PASS src/services/database-cleanup-job.test.ts + ● Console + + console.log + 2026-10-16T12:10:34.734Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:35.231Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:35.318Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:35.375Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/api-version-registry.test.ts +PASS src/services/notification-api.test.ts + ● Console + + console.log + 2026-10-16T12:10:38.093Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:38.147Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:38.150Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:38.169Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:38.176Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:38.212Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:79:22) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:89:22) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:98:7) + + ● WebhookDeliveryService › successful delivery (2xx) › passes the payload and options to sendWebhook + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:109:7) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:138:7) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:167:7) + + ● WebhookDeliveryService › request timeout (AbortError) › returns success: false when the request times out + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:183:22) + + ● WebhookDeliveryService › request timeout (AbortError) › logs a warn for timeouts so the caller knows to retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:193:7) + + ● WebhookDeliveryService › request timeout (AbortError) › includes the configured timeout in the errorReason + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:205:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › returns success: false on a generic network +error + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:217:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › logs an error for non-timeout network failures + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:226:7) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › handles non-Error thrown values gracefully + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:237:22) + + ● WebhookDeliveryService › default options › uses the instance-level timeout when none provided per-call + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:251:7) + + ● WebhookDeliveryService › default options › merges instance headers with per-call headers + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:264:7) + + ● WebhookDeliveryService › failure classification › labels a request timeout as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:282:22) + + ● WebhookDeliveryService › failure classification › labels a TimeoutError as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:292:22) + + ● WebhookDeliveryService › failure classification › labels a generic network error as "network" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:300:22) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:309:22) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:317:22) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:325:22) + +PASS src/services/notification-analytics-aggregator.test.ts +FAIL src/services/scheduled-notification-queue.test.ts (11.285 s) + ● Console + + console.log + 2026-10-16T12:10:41.813Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:42.751Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:42.757Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:42.766Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:42.885Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:42.934Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.004Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.098Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.133Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.168Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.217Z info: Fetched and locked pending notifications +{"count":1,"processorId":"restarted-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.279Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:43.334Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.270Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.272Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.278Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.303Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.327Z info: Fetched and locked pending notifications +{"count":1,"processorId":"doomed-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.342Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.348Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.371Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.375Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.383Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.412Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.446Z info: Fetched and locked pending notifications +{"count":1,"processorId":"next-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.449Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:45.462Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.375Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.378Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.380Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.404Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.420Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.440Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.443Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.447Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.467Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.468Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.469Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.476Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:46.483Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.436Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.437Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.442Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.462Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.478Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.495Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.513Z info: Fetched and locked pending notifications +{"count":3,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.519Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:47.530Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.285Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.287Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.290Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.309Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.324Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.343Z info: Fetched and locked pending notifications +{"count":2,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.460Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.466Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:48.473Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.277Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.279Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.280Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.354Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.372Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.428Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.432Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:49.442Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.063Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.065Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.068Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.090Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.108Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.126Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:11:51.110Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.162Z info: Fetched due retries {"count":1,"processorId":"retry-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.166Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.174Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.937Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.941Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.943Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.960Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:51.975Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:52.007Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent outage"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:52.010Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:10:52.018Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +ts-jest[ts-compiler] (WARN) src/services/notification-retry-queue.test.ts:51:66 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +51 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:61:66 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +61 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:74:66 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +74 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:95:9 - error TS2353: Object +literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +95 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:119:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +119 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:138:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +138 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:160:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +160 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:187:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +187 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:213:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +213 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:247:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +247 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:271:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +271 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:287:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +287 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:325:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +325 baseDelayMs: 100, +   ~~~~~~~~~~~ +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   109 | await Promise.resolve(); +  110 | await Promise.resolve(); + > 111 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  112 | +  113 | queue.stop(); +  114 | }); + + at Object. (src/services/notification-retry-queue.test.ts:111:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   128 | await Promise.resolve(); +  129 | + > 130 | expect(queue.size()).toBe(0); +  | ^ +  131 | queue.stop(); +  132 | }); +  133 | + + at Object. (src/services/notification-retry-queue.test.ts:130:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4493.069588561699, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:10:58.410Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   146 | await Promise.resolve(); +  147 | + > 148 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  149 | 'Retry succeeded', +  150 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  151 | ); + + at Object. (src/services/notification-retry-queue.test.ts:148:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   170 | await Promise.resolve(); +  171 | await Promise.resolve(); + > 172 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  173 | +  174 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  175 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:172:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   198 | await Promise.resolve(); +  199 | + > 200 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  201 | 'Retry failed, scheduling next attempt', +  202 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  203 | ); + + at Object. (src/services/notification-retry-queue.test.ts:200:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   225 | jest.advanceTimersByTime(100); +  226 | await flush(); + > 227 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  228 | +  229 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  230 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:227:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   257 | await Promise.resolve(); +  258 | + > 259 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  260 | 'Notification permanently failed after max retries', +  261 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  262 | ); + + at Object. (src/services/notification-retry-queue.test.ts:259:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue.test.ts:299:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   349 | +  350 | // The notification fn was called exactly 3 times + > 351 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  352 | +  353 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  354 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:351:30) + +PASS src/api/content-type.test.ts +ts-jest[ts-compiler] (WARN) src/utils/logger.test.ts:191:27 - error TS2345: +Argument of type '(_info: unknown, next: () => void) => void' is not assignable to parameter of type '(...args: +unknown[]) => any'. + Types of parameters 'next' and 'args' are incompatible. + Type 'unknown' is not assignable to type '() => void'. + +191 .mockImplementation((_info: unknown, next: () => void) => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/utils/logger.test.ts:310:27 - error TS2345: Argument of type '(_info: +unknown, next: () => void) => void' is not assignable to parameter of type '(...args: unknown[]) => any'. + Types of parameters 'next' and 'args' are incompatible. + Type 'unknown' is not assignable to type '() => void'. + +310 .mockImplementation((_info: unknown, next: () => void) => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PASS src/utils/logger.test.ts +PASS src/api/notifications-history.test.ts +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-backfill.test.ts:51:3 - +error TS1117: An object literal cannot have multiple properties with the same name. + +51 validateRpcResponse: jest.fn().mockReturnValue({ valid: true }), +   ~~~~~~~~~~~~~~~~~~~ +src/services/event-subscriber-backfill.test.ts:83:3 - error TS2741: +Property 'eventBatchSize' is missing in type '{ stellarNetwork: string; stellarNetworkPassphrase: string; +stellarRpcUrl: string; contractAddresses: ContractConfig[]; pollIntervalMs: number; maxReconnectAttempts: number; +reconnectDelayMs: number; eventsApiPort: number; eventsApiCorsOrigin: string; backfill: { ...; } | undefined; }' but +required in type 'Config'. + +83 return { +   ~~~~~~ + + src/types/index.ts:72:3 + 72 eventBatchSize: number; +    ~~~~~~~~~~~~~~ + 'eventBatchSize' is declared here. +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (53.723 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   168 | +  169 | describe('fan-out delivery', () => { + > 170 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  171 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  172 | +  173 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   177 | }); +  178 | + > 179 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  180 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  181 | +  182 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:179:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   193 | +  194 | describe('channel-specific payloads', () => { + > 195 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  196 | await deliverToAllChannels( +  197 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  198 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   209 | }); +  210 | + > 211 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  212 | await deliverToAllChannels( +  213 | createMockEvent({ id: 'event-system', type: +'system' }) +  214 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:211:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   224 | +  225 | describe('partial delivery failure', () => { + > 226 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  227 | +router.setHandler(channels[1].config.webhookUrl, () => +  228 | failedFetch(500, 'Internal Server Error') +  229 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   234 | }); +  235 | + > 236 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  237 | +router.setHandler(channels[1].config.webhookUrl, () => +  238 | failedFetch(429, 'Too Many Requests', +'rate limited') +  239 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:236:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   259 | }); +  260 | + > 261 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  262 | +router.setHandler(channels[2].config.webhookUrl, () => { +  263 | throw new Error('ECONNRESET'); +  264 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:261:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   288 | }; +  289 | + > 290 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  291 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  292 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  293 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:290:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   314 | }); +  315 | + > 316 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  317 | // 'audit' never recovers. +  318 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  319 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:316:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   339 | }); +  340 | + > 341 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  342 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  343 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  344 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:341:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + +ts-jest[ts-compiler] (WARN) src/services/notification-retry-queue-refactored.test.ts:38:66 +- error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist +in type 'RetryQueueOptions'. + +38 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:50:66 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +50 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:71:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +71 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:96:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +96 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:116:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +116 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:145:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +145 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:173:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +173 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:204:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +204 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:240:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +240 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:269:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +269 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:286:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +286 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:308:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +308 baseDelayMs: 100, +   ~~~~~~~~~~~ +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   86 | await Promise.resolve(); +  87 | await Promise.resolve(); + > 88 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  89 | +  90 | queue.stop(); +  91 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:88:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   106 | await Promise.resolve(); +  107 | + > 108 | expect(queue.size()).toBe(0); +  | ^ +  109 | queue.stop(); +  110 | }); +  111 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:108:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 5610.902651429087, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:11:12.976Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   131 | await Promise.resolve(); +  132 | + > 133 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  134 | 'Retry succeeded', +  135 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  136 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:133:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   156 | await Promise.resolve(); +  157 | await Promise.resolve(); + > 158 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  159 | +  160 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  161 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:158:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   189 | await Promise.resolve(); +  190 | + > 191 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  192 | 'Retry failed, scheduling next attempt', +  193 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  194 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:191:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   218 | jest.advanceTimersByTime(100); +  219 | await flush(); + > 220 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  221 | +  222 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  223 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:220:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   255 | await Promise.resolve(); +  256 | + > 257 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  258 | 'Notification permanently failed after max retries', +  259 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  260 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:257:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:299:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   326 | } +  327 | + > 328 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  329 | queue.stop(); +  330 | }); +  331 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:328:30) + +FAIL src/api/execution-metrics-api.test.ts (7.419 s) + ● Console + + console.log + 2026-10-16T12:11:01.250Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.085Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.088Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.098Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.120Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.151Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.181Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.215Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.398Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:02.415Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.202Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.204Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.208Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.228Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.263Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.278Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.353Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.412Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.452Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.521Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.606Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.630Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.710Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.735Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.864Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:03.916Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:04.902Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:04.909Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:04.911Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:04.954Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:04.965Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:05.796Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:05.798Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:05.799Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:05.829Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:05.835Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:06.747Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:06.748Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:06.750Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:06.770Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:06.796Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:07.376Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:07.408Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +PASS src/services/subscription-service.test.ts +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/api/template-routes.test.ts + ● Console + + console.log + 2026-10-16T12:11:12.587Z error: Failed to list templates {"error":{"message":"channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:12.619Z error: Failed to list templates {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n at sendOk +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n +at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:193:11)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:175:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:12.677Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:12.695Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:85:33) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:107:33) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:117:33) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:137:33) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:153:33) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › GET /api/templates › accepts a valid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:5) + at Object. (src/api/template-routes.test.ts:175:7) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:201:33) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:215:33) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:226:33) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:237:33) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:247:33) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:261:33) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +PASS src/api/templates-api.test.ts +PASS src/api/delivery-metrics-api.test.ts + ● Console + + console.log + 2026-10-16T12:11:14.207Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"df8f8f08" +,"correlationId":"d79962a5-93f9-416d-b3c6-2b09b0af7203","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.253Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"4cf79b83" +,"correlationId":"9faf02b9-aecd-42bd-a4c8-a35e9be05cee","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.274Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"652e0bbf" +,"correlationId":"ed1bfb2b-90b2-4ff2-b4ed-31bbd5ed45f7","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs" +:0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.278Z info: Delivery metrics window reset after read +{"requestId":"652e0bbf","correlationId":"ed1bfb2b-90b2-4ff2-b4ed-31bbd5ed45f7"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.293Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"4a33339e" +,"correlationId":"55a5de02-acb4-442d-9581-58f1ef899adc","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.306Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"22a08c21" +,"correlationId":"a596af6d-b79b-48bc-92cd-46f948642700","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.320Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"f7e8b2cd","correlationId":"0ba50db6-a7ef-4080-9447-ea90aa3dd28a","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.327Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"f7e8b2cd","correlationId":"0ba50db6-a7ef-4080-9447-ea90aa3dd28a","count":1,"durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.352Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"e09532cd","correlationId":"c4f84863-9690-48d3-8f60-6405a7dce017","limit":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.356Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"e09532cd","correlationId":"c4f84863-9690-48d3-8f60-6405a7dce017","count":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.371Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"5 +44fe4d6","correlationId":"ea92963e-fc96-4902-be22-d6221ce230b6","limit":50,"since":"2024-01-01T00:00:00.000Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.374Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"544fe4d6","correlationId":"ea92963e-fc96-4902-be22-d6221ce230b6","count":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.404Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"6e97bf58","correlationId":"51d1dbad-08c8-4e93-b9d0-3a99704637d6","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:14.410Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"6e97bf58","correlationId":"51d1dbad-08c8-4e93-b9d0-3a99704637d6","count":0,"durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/examples/webhook-consumer-verification-example.test.ts +PASS src/services/subscription-event-emitter.test.ts +PASS src/middleware/body-limit.test.ts +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +PASS src/services/notification-stats-cache.test.ts +PASS src/__tests__/shutdown-unit.test.ts +PASS src/services/rpc-rate-limiter.test.ts + ● Console + + console.log + 2026-10-16T12:11:17.633Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:17.705Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:17.768Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:17.831Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:17.893Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.067Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.081Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.097Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.112Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.131Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.144Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.160Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.175Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.191Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.207Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.225Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.239Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.254Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.270Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.286Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.302Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.317Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.333Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.349Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.364Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.380Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.395Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.412Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.428Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.443Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.459Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.475Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.491Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.507Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.522Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.537Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.553Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.568Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.585Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.600Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.616Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.632Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.648Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.663Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.679Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.694Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.710Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.725Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.741Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.756Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.771Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.787Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.803Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.818Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.834Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.850Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.866Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.881Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.897Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.913Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.928Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.944Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.960Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.975Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:18.991Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.007Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.022Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.037Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.053Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-scheduler.test.ts + ● Console + + console.log + 2026-10-16T12:11:19.935Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.958Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.960Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.961Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.969Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.971Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.973Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.975Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.981Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.986Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.992Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:19.997Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:20.002Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load-test-runner.test.ts +PASS src/api/events-server.health.test.ts +PASS src/services/idempotency-key-service.test.ts +PASS src/services/circuit-breaker.test.ts + ● Console + + console.log + 2026-10-16T12:11:24.418Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.435Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.440Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.442Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.444Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.447Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.451Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.452Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.454Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152685454} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.456Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.460Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.461Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.463Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152685463} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.465Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.467Z warn: Circuit breaker is OPEN, blocking request +{"state":"open","nextAttemptTime":1792152685463} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.470Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.473Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.475Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152685475} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:24.477Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:25.586Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:25.591Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:25.593Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:25.594Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152686594} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:25.596Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.711Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.713Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.716Z info: Circuit breaker closed after successful recovery +{"successCount":0,"successThreshold":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.718Z info: Circuit breaker recorded success +{"state":"closed","successCount":0,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.721Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.726Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.728Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152687728} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:26.730Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.848Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.850Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.852Z error: Circuit breaker opened due to repeated failures +{"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152688852} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.854Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.857Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.860Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.862Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152688861} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.864Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.866Z info: Circuit breaker manually reset {"previousState":"closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.871Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.873Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.875Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.877Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.879Z error: Circuit breaker opened due to repeated failures +{"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792152688879} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.881Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":5,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:27.887Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/__tests__/retry-scheduler.integration.test.ts (10.595 s) + ● Console + + console.log + 2026-10-16T12:11:31.049Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:31.205Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:31.250Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:31.284Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:31.346Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +PASS src/utils/circuit-breaker.test.ts +PASS src/services/notification-template-repository.test.ts + ● Console + + console.log + 2026-10-16T12:11:37.417Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.478Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.481Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.487Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.493Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.506Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.512Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.526Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.528Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.530Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.542Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.546Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.554Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.555Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.556Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.560Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.563Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.575Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.578Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.580Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.582Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.586Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.598Z info: Notification template updated +{"templateId":"tmpl-001","actor":"admin@example.com"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.602Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.605Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.621Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.624Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.626Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.631Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.634Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.636Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.643Z info: Template audit record created +{"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.648Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.651Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.654Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.683Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.686Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.687Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.697Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.701Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.705Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.725Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.727Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.729Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.732Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.736Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.742Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.756Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.758Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.760Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.762Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.766Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.768Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.781Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.783Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.784Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.786Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.789Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.799Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.801Z info: [TemplateCache] Template invalidated +{"templateId":"tmpl-001","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.803Z info: Notification template updated +{"templateId":"tmpl-001","actor":"cache-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.804Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.808Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.820Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.821Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.823Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.825Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.828Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.830Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.833Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET +actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.834Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.836Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.849Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.851Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.852Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.854Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.858Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.860Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.869Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log +WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.871Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.875Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.886Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.888Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.888Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.893Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.896Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.909Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.910Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.911Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:37.914Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/logger-structured.test.ts +PASS src/services/payload-validation.integration.test.ts (10.688 s) +PASS src/services/scheduled-notification-repository-cache.test.ts + ● Console + + console.log + 2026-10-16T12:11:40.345Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.376Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.379Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.386Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.425Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.448Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.454Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.483Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.488Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.492Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.498Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.502Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.510Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.514Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.516Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.520Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.528Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + Performance comparison: + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:130:15) + + console.log + Uncached call: 2ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:131:15) + + console.log + Cached call: 0ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:132:15) + + console.log + Improvement: 100.0% + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:133:15) + + console.log + 2026-10-16T12:11:40.538Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.542Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.545Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.546Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.549Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.553Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.554Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.559Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:40.575Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/search-suggestion.test.ts + ● Console + + console.log + 2026-10-16T12:11:41.761Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:42.052Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:42.061Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:42.087Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:42.504Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/config-logging.test.ts +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts (11.835 s) + ● Console + + console.log + 2026-10-16T12:11:52.305Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.419Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.426Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.435Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.899Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.904Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.909Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:52.931Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:53.799Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:53.807Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:53.817Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:53.823Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:54.315Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:54.319Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:54.329Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:54.345Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +ts-jest[ts-compiler] (WARN) src/tests/events-server.test.ts:9:4 - error +TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value. + +9 ): Promise<{ status: number; headers: http.IncomingHttpHeaders }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:10:14 - error TS2552: Cannot find name +'headers'. Did you mean 'Headers'? + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~~ + + node_modules/@types/node/web-globals/fetch.d.ts:47:9 + 47 var Headers: typeof globalThis extends { onmessage: any; Headers: infer T } ? T : typeof +undici.Headers; +    ~~~~~~~ + 'Headers' is declared here. +src/tests/events-server.test.ts:10:24 - error TS2693: 'Record' only +refers to a type, but is being used as a value here. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:10:48 - error TS2304: Cannot find name +'method'. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:10:57 - error TS2693: 'string' only +refers to a type, but is being used as a value here. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:11:4 - error TS2365: Operator '<' cannot +be applied to types 'PromiseConstructor' and '{ status: any; headers: any; body: any; }'. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:11:4 - error TS2365: Operator '>' cannot +be applied to types 'boolean' and '{ return: Promise; }'. + + 11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 return new Promise((resolve, reject) => { +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 26 }); +  ~~~~~ + 27 } +  ~ +src/tests/events-server.test.ts:11:22 - error TS2693: 'number' only +refers to a type, but is being used as a value here. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~ +src/tests/events-server.test.ts:11:44 - error TS2339: Property +'IncomingHttpHeaders' does not exist on type 'typeof import("http")'. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:11:71 - error TS2693: 'any' only refers +to a type, but is being used as a value here. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~ +src/tests/events-server.test.ts:150:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +150 const { status, body } = await makeRequest('/api/events'); +   ~~~~ +src/tests/events-server.test.ts:157:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +157 const { status, body } = await makeRequest('/api/events?limit=50'); +   ~~~~ +src/tests/events-server.test.ts:163:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +163 const { status, body } = await makeRequest('/api/events?limit=200'); +   ~~~~ +src/tests/events-server.test.ts:169:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +169 const { status, body } = await makeRequest('/api/events?limit=-10'); +   ~~~~ +src/tests/events-server.test.ts:175:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +175 const { status, body } = await makeRequest('/api/events?limit=invalid'); +   ~~~~ +src/tests/events-server.test.ts:10:22 - error TS1109: Expression expected. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:10:55 - error TS1109: Expression expected. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:10:66 - error TS2809: Declaration or +statement expected. This '=' follows a block of statements, so if you intended to write a destructuring assignment, +you might need to wrap the whole assignment in parentheses. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:11:1 - error TS1128: Declaration or +statement expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +  ~ +src/tests/events-server.test.ts:11:2 - error TS1128: Declaration or +statement expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:11:28 - error TS1005: ',' expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:11:63 - error TS1005: ',' expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:12:10 - error TS1005: ':' expected. + +12 return new Promise((resolve, reject) => { +   ~~~ +src/tests/events-server.test.ts:23:5 - error TS1128: Declaration or +statement expected. + +23 ); +   ~ +src/tests/events-server.test.ts:26:5 - error TS1005: ',' expected. + +26 }); +   ~ +src/tests/events-server.test.ts:180:1 - error TS1005: '}' expected. + +180 +   + + src/tests/events-server.test.ts:77:46 + 77 describe('GET /api/events pagination', () => { +    ~ + The parser expected to find a '}' to match the '{' token here. +FAIL src/tests/events-server.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\tests\events-server.test.ts:11 + headers ? : Record; + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › isDuplicate › returns true for a fingerprint that has been marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:41:9) + + ● NotificationDeduplicator › isDuplicate › returns false for a different fingerprint after another is marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:47:9) + + ● NotificationDeduplicator › markSent › increments size after marking a new fingerprint + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:55:9) + + ● NotificationDeduplicator › markSent › does not increment size when marking the same fingerprint twice + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:61:9) + + ● NotificationDeduplicator › markSent › evicts the oldest entry when the cache is full + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:68:9) + + ● NotificationDeduplicator › markSent › logs a warning when evicting due to full cache + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:83:9) + + ● NotificationDeduplicator › markSent › does not evict when cache has room + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:95:9) + + ● NotificationDeduplicator › time window and metrics › allows the same fingerprint after the deduplication window +expires + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:109:9) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:120:9) + + ● NotificationDeduplicator › time window and metrics › counts evicted and expired entries in metrics + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:140:9) + + ● NotificationDeduplicator › size › returns the correct count after multiple unique fingerprints + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:163:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › prevents a second notification for the +same event on re-poll + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:175:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › allows notifications for unique events +with the same contract + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:182:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › treats same event id on different +contracts as distinct + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:190:9) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +PASS src/services/worker-manager.test.ts + ● Console + + console.log + 2026-10-16T12:11:58.134Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.163Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.167Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.169Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.179Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.180Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.182Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.185Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.186Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.187Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:58.193Z info: Initiating graceful shutdown {"activeJobs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:59.207Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:59.219Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:59.222Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:59.229Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:11:59.232Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.238Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.240Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.243Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.246Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.247Z warn: Graceful shutdown already initiated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.250Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:00.253Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.257Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/api/archive-api.test.ts + ● Console + + console.log + 2026-10-16T12:12:00.538Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.570Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an +integer, received 'abc'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.577Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.579Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= +100"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.582Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.584Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= +0"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.586Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.589Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be +one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.592Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.595Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be +a valid date, received 'not-a-date'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.599Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.621Z error: Failed to query archive {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n at sendOk +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n +at handleArchiveRequest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:116:13)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\archive-api.test.ts:101:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.626Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.629Z error: Failed to query archive {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n at sendOk +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n +at handleArchiveRequest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:116:13)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\archive-api.test.ts:113:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.642Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.645Z error: Failed to fetch archive record {"error":{"message":"res.setHeader is +not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n +at sendErr (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\response.ts:155:3)\n at handleArchiveRequest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:76:16)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\archive-api.test.ts:145:7)"},"requestId":"req-1","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.651Z info: Handling POST /api/archive/run {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:00.654Z error: Archive run failed {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:120:7)\n at sendOk +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\response.ts:138:3)\n +at handleArchiveRequest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\archive-api.ts:55:13)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\archive-api.test.ts:167:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● archive-api validation › GET /api/archive › accepts a request with valid filters + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:125:14) + at Object. (src/api/archive-api.test.ts:101:7) + + ● archive-api validation › GET /api/archive › accepts a request with no filters at all + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:125:14) + at Object. (src/api/archive-api.test.ts:113:7) + + ● archive-api validation › GET /api/archive/:id › accepts a valid positive integer id + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:82:14) + at Object. (src/api/archive-api.test.ts:145:7) + + ● archive-api validation › POST /api/archive/run › returns 503 when the archive service is not configured + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:49:14) + at Object. (src/api/archive-api.test.ts:157:33) + + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:58:14) + at Object. (src/api/archive-api.test.ts:167:7) + +PASS src/utils/validation.test.ts +ts-jest[ts-compiler] (WARN) src/services/event-normalization.test.ts:101:35 - +error TS2353: Object literal may only specify known properties, and 'payload_version' does not exist +in type 'ScMapEntry[]'. + +101 value: xdr.ScVal.scvMap({ payload_version: 1, notification_id: 'abc' }), +   ~~~~~~~~~~~~~~~ +PASS src/services/event-normalization.test.ts +FAIL src/database/event-notification-indexes.test.ts + ● Console + + console.log + 2026-10-16T12:12:01.631Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-24164-1792152721620.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:04.213Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:04.218Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:04.227Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-24164-1792152721620.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:04.240Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, +created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILE +D","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:04.266Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +PASS src/schema/compatibility-check.test.ts +PASS src/services/template-service.test.ts + ● Console + + console.log + 2026-10-16T12:12:07.587Z info: Template created successfully +{"templateId":1,"uniqueKey":"welcome-email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.733Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome +{{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.739Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" +","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.745Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.750Z error: Failed to create template {"error":{"message":"description: must be a +string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","description":12345}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.754Z error: Failed to create template {"error":{"message":"name: must not exceed +255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTe +mplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.759Z error: Failed to create template {"error":{"message":"variables: must be an +array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","variables":[1,2,3]}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.764Z error: Failed to create template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","defaultValues":"nope"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.769Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.772Z error: Failed to update template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.776Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.779Z error: Failed to update template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/request-id.test.ts +FAIL src/tests/payload-integrity.test.ts (5.697 s) + ● Console + + console.log + 2026-10-16T12:12:05.500Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.405Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.409Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.425Z info: Database initialized successfully {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.430Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:07.767Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:08.132Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:08.399Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:08.695Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:08.964Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:09.494Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:09.698Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:09.748Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +PASS src/__tests__/api-contract.test.ts + ● Console + + console.log + 2026-10-16T12:12:12.028Z info: Handling GET /api/events +{"requestId":"b157e567","correlationId":"cbb01e76-3093-4bbe-ba9e-5f2f3f8627f3","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.109Z info: GET /api/events complete +{"requestId":"b157e567","correlationId":"cbb01e76-3093-4bbe-ba9e-5f2f3f8627f3","returned":0,"durationMs":85} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.148Z warn: Unhandled request {"requestId":"898874fb","correlationId":"7dc837cd-51c7 +-413d-8062-168b155d119d","method":"GET","url":"/api/definitely-not-a-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.153Z info: Request completed +{"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":5,"requestId":"898874fb"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.172Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.186Z info: Handling GET /api/analytics {"requestId":"674a4841","correlationId":"ab7 +1b687-c142-4934-aa93-29b0284967e0","totalRecorded":0,"reset":false,"durationMs":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.206Z info: Handling GET /api/events +{"requestId":"03b46da8","correlationId":"2a9dd4bf-dc07-4864-8a5b-5f4be2d1aa02","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:12.213Z info: GET /api/events complete +{"requestId":"03b46da8","correlationId":"2a9dd4bf-dc07-4864-8a5b-5f4be2d1aa02","returned":0,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/notification-api-webhook.test.ts + ● Console + + console.log + 2026-10-16T12:12:13.621Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.650Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.655Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.661Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.665Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.695Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.720Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.737Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:13.746Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +PASS src/services/notification-search-service.test.ts + ● Console + + console.log + 2026-10-16T12:12:15.183Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:15.236Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:15.239Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:15.248Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:15.254Z info: Notification search complete +{"total":1,"returned":1,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/scripts/replay-event.test.ts (13.712 s) + ● Console + + console.log + 2026-10-16T12:12:25.869Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:25.960Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:25.970Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:25.976Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:25.985Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:25.991Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.004Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.006Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.008Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.012Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.016Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.030Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.032Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.034Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.037Z info: Event processed and recorded {"eventId":"duplicate-test-event","contract +Address":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a0 +26ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.046Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.049Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.061Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.063Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.064Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.066Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAd +dress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de +1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.069Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.072Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.086Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.088Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.090Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.094Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.097Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.111Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.112Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.113Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.115Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.118Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.131Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.133Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.134Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:26.136Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/config-schema.test.ts +FAIL src/tests/api-versioning.test.ts + ● Console + + console.log + 2026-10-16T12:12:30.921Z info: Handling GET /api/events +{"requestId":"1d8491a0","correlationId":"9c23890d-9da8-4aca-8e01-7f08da04952d","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.952Z info: GET /api/events complete +{"requestId":"1d8491a0","correlationId":"9c23890d-9da8-4aca-8e01-7f08da04952d","returned":0,"durationMs":32} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.970Z info: Handling GET /api/events +{"requestId":"43abfcf6","correlationId":"ac8c90b3-3d04-4683-a18a-edea912df841","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.981Z info: GET /api/events complete +{"requestId":"43abfcf6","correlationId":"ac8c90b3-3d04-4683-a18a-edea912df841","returned":0,"durationMs":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.992Z info: Handling GET /api/events +{"requestId":"9df14ba5","correlationId":"4ea92647-56a4-40ac-82cf-8eb5330b42e3","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.995Z info: GET /api/events complete +{"requestId":"9df14ba5","correlationId":"4ea92647-56a4-40ac-82cf-8eb5330b42e3","returned":0,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.003Z info: Handling GET /api/events +{"requestId":"9e54d046","correlationId":"c480ea8c-856a-43aa-a9d3-fd9ed8c5c8f8","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.011Z info: GET /api/events complete +{"requestId":"9e54d046","correlationId":"c480ea8c-856a-43aa-a9d3-fd9ed8c5c8f8","returned":0,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.018Z info: Handling GET /api/events +{"requestId":"fe002b28","correlationId":"7c18d430-ef48-436b-a57a-6d8e33179b35","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.021Z info: GET /api/events complete +{"requestId":"fe002b28","correlationId":"7c18d430-ef48-436b-a57a-6d8e33179b35","returned":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.043Z info: Handling GET /api/events +{"requestId":"81fa84e0","correlationId":"3dc88159-e42d-418b-a610-82cd24a0f25a","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.045Z info: GET /api/events complete +{"requestId":"81fa84e0","correlationId":"3dc88159-e42d-418b-a610-82cd24a0f25a","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.047Z info: Handling GET /api/events +{"requestId":"48b4e840","correlationId":"8edaff2f-f1ef-4b12-b2b3-7e4cbd22cc78","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.050Z info: GET /api/events complete +{"requestId":"48b4e840","correlationId":"8edaff2f-f1ef-4b12-b2b3-7e4cbd22cc78","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.060Z info: Handling GET /api/events +{"requestId":"3e2107ae","correlationId":"514e0f4c-6191-440d-8169-e7982e9e75a1","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.062Z info: GET /api/events complete +{"requestId":"3e2107ae","correlationId":"514e0f4c-6191-440d-8169-e7982e9e75a1","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.067Z info: Handling GET /api/events +{"requestId":"4b009627","correlationId":"c05a4901-f1f1-4c24-9027-b22055349aed","limit":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.070Z info: GET /api/events complete +{"requestId":"4b009627","correlationId":"c05a4901-f1f1-4c24-9027-b22055349aed","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.086Z warn: Unhandled request {"requestId":"bd518e32","correlationId":"cf59875b-6c36 +-4b1a-b6c3-c0322c09d5a3","method":"GET","url":"/api/v1/unknown-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.091Z info: Request completed +{"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":5,"requestId":"bd518e32"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.106Z warn: Unhandled request {"requestId":"fabd34f4","correlationId":"243cd887-9e24 +-40dc-b496-2a8413cc2d3a","method":"GET","url":"/api/v1/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:31.110Z info: Request completed +{"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":4,"requestId":"fabd34f4"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +PASS src/services/event-store-consistency.test.ts + ● Console + + console.log + 2026-10-16T12:12:32.266Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.286Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.288Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.293Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.322Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/__tests__/database-recovery.test.ts (6.336 s) + ● Console + + console.log + 2026-10-16T12:12:33.313Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   29 | }); +  30 | + > 31 | test('recovers transparently when the handle is closed +mid-flight', async () => { +  | ^ +  32 | const db = new Database(testDbPath, { +maxReconnectAttempts: 3, reconnectBaseDelayMs: 10 }); +  33 | await db.initialize(); +  34 | await db.run('CREATE TABLE t (id INTEGER PRIMARY KEY, v +TEXT)'); + + at src/__tests__/database-recovery.test.ts:31:3 + at Object. (src/__tests__/database-recovery.test.ts:18:1) + + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + +FAIL src/__tests__/scheduler-concurrency.test.ts (23.007 s) + ● Console + + console.log + 2026-10-16T12:12:27.661Z info: Connected to SQLite database +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.507Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.517Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.522Z info: Database initialized successfully +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.584Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.646Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.654Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.669Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.676Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.700Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.704Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:28.722Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.066Z info: Fetched and locked pending notifications +{"requestId":"2bdf8c9c","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.079Z info: Processing batch of scheduled notifications +{"requestId":"2bdf8c9c","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.085Z info: Processing scheduled notification +{"requestId":"2bdf8c9c","id":1,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.194Z info: Notification marked as completed {"requestId":"2bdf8c9c","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.216Z info: Notification delivered successfully +{"requestId":"2bdf8c9c","id":1,"type":"discord","durationMs":90} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.219Z info: Processing scheduled notification +{"requestId":"2bdf8c9c","id":2,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.318Z info: Notification marked as completed {"requestId":"2bdf8c9c","id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.336Z info: Notification delivered successfully +{"requestId":"2bdf8c9c","id":2,"type":"discord","durationMs":86} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.339Z info: Processing scheduled notification +{"requestId":"2bdf8c9c","id":3,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.451Z info: Notification marked as completed {"requestId":"2bdf8c9c","id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.464Z info: Notification delivered successfully +{"requestId":"2bdf8c9c","id":3,"type":"discord","durationMs":99} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.467Z info: Processing scheduled notification +{"requestId":"2bdf8c9c","id":4,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.577Z info: Notification marked as completed {"requestId":"2bdf8c9c","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.597Z info: Notification delivered successfully +{"requestId":"2bdf8c9c","id":4,"type":"discord","durationMs":95} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:30.600Z info: Scheduler batch complete +{"requestId":"2bdf8c9c","processorId":"test-uuid-0001","count":4,"durationMs":552} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.605Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.651Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.670Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.671Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.686Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.688Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.709Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.713Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.733Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.735Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.750Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.752Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:30.768Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.104Z info: Fetched and locked pending notifications +{"requestId":"9d8cf3d1","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.107Z info: Processing batch of scheduled notifications +{"requestId":"9d8cf3d1","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.111Z info: Processing scheduled notification +{"requestId":"9d8cf3d1","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:32.113Z info: Processing scheduled notification +{"requestId":"9d8cf3d1","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:32.115Z info: Processing scheduled notification +{"requestId":"9d8cf3d1","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:12:32.244Z info: Notification marked as completed {"requestId":"9d8cf3d1","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:32.247Z info: Notification marked as completed {"requestId":"9d8cf3d1","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:32.249Z info: Notification marked as completed {"requestId":"9d8cf3d1","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:12:32.286Z info: Notification delivered successfully +{"requestId":"9d8cf3d1","id":5,"type":"discord","durationMs":96} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:32.289Z info: Processing scheduled notification +{"requestId":"9d8cf3d1","id":8,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:32.293Z info: Notification delivered successfully +{"requestId":"9d8cf3d1","id":6,"type":"discord","durationMs":97} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:32.296Z info: Processing scheduled notification +{"requestId":"9d8cf3d1","id":9,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:32.298Z info: Notification delivered successfully +{"requestId":"9d8cf3d1","id":7,"type":"discord","durationMs":96} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:12:32.301Z info: Processing scheduled notification +{"requestId":"9d8cf3d1","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:12:32.428Z info: Notification marked as completed {"requestId":"9d8cf3d1","id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:32.430Z info: Notification marked as completed {"requestId":"9d8cf3d1","id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:32.433Z info: Notification marked as completed {"requestId":"9d8cf3d1","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:12:32.467Z info: Notification delivered successfully +{"requestId":"9d8cf3d1","id":8,"type":"discord","durationMs":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:32.469Z info: Notification delivered successfully +{"requestId":"9d8cf3d1","id":9,"type":"discord","durationMs":91} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:32.472Z info: Notification delivered successfully +{"requestId":"9d8cf3d1","id":10,"type":"discord","durationMs":86} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:12:32.475Z info: Scheduler batch complete +{"requestId":"9d8cf3d1","processorId":"test-uuid-0002","count":6,"durationMs":391} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.480Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.508Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.522Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.524Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:32.539Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:35.654Z info: Fetched and locked pending notifications +{"requestId":"e682b23a","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:35.657Z info: Processing batch of scheduled notifications +{"requestId":"e682b23a","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:35.662Z info: Processing scheduled notification +{"requestId":"e682b23a","id":11,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:35.665Z info: Processing scheduled notification +{"requestId":"e682b23a","id":12,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:38.579Z info: Notification marked as completed {"requestId":"e682b23a","id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:38.583Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:38.599Z info: Notification marked as completed {"requestId":"e682b23a","id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:40.196Z info: Notification delivered successfully +{"requestId":"e682b23a","id":11,"type":"discord","durationMs":91} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:12:40.199Z info: Notification delivered successfully +{"requestId":"e682b23a","id":12,"type":"discord","durationMs":90} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:12:40.203Z info: Scheduler batch complete +{"requestId":"e682b23a","processorId":"test-uuid-0003","count":2,"durationMs":6352} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:40.271Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   123 | }); +  124 | + > 125 | test('concurrency larger than the batch is clamped to the batch +size', async () => { +  | ^ +  126 | await seedDueNotifications(2); +  127 | await sleep(1300); +  128 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:125:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +PASS src/utils/payload-size-validator.test.ts +PASS src/services/notification-template-cache.test.ts + ● Console + + console.log + 2026-10-16T12:12:42.409Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.444Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.452Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.461Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.469Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.475Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.484Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.487Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.493Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.497Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.501Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.504Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.511Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.514Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.519Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.527Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.530Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.532Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.535Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.537Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.548Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.552Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.555Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.559Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.562Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.567Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.571Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:42.577Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/store/preference-store.test.ts +PASS src/services/backpressure-controller.test.ts + ● Console + + console.log + 2026-10-16T12:12:43.910Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.934Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.937Z info: Backpressure deactivated: queue recovered +{"queueSize":49,"threshold":50,"durationMs":4,"targetThroughputPerSec":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.940Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.945Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.951Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.956Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.961Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:43.966Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/store/event-registry.test.ts +PASS src/services/notification-template-service.test.ts + ● Console + + console.log + 2026-10-16T12:12:45.067Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.088Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.091Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.097Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.101Z info: Notification template created {"templateId":"svc-template"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.112Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.120Z info: Template audit record created +{"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.127Z info: [TemplateCache] Template invalidated +{"templateId":"svc-template","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.129Z info: Notification template updated +{"templateId":"svc-template","actor":"service-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.132Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.137Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.139Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.153Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.155Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.156Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.161Z info: Notification template created {"templateId":"tmpl-a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.162Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.165Z info: Notification template created {"templateId":"tmpl-b"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.167Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.170Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.173Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.189Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.191Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.194Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.197Z info: Notification template created {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.199Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.202Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.203Z info: Notification template deleted {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.204Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.210Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.214Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.231Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.233Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.235Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.239Z info: Notification template created {"templateId":"render-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.240Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.245Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.250Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.266Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.268Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.269Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.273Z info: Notification template created {"templateId":"render-missing"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.275Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.283Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.286Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.302Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.303Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.305Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.307Z info: Notification template created {"templateId":"no-vars"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.311Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:45.313Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-statistics.test.ts +PASS src/__tests__/load.test.ts + ● Console + + console.log + + === Load Test Results (100 events) === + + at Object. (src/__tests__/load.test.ts:61:15) + + console.log + Total processing time: 176ms + + at Object. (src/__tests__/load.test.ts:62:15) + + console.log + Throughput: 568.18 events/second + + at Object. (src/__tests__/load.test.ts:63:15) + + console.log + Average latency per event: 1.76ms + + at Object. (src/__tests__/load.test.ts:64:15) + + console.log + + === Load Test Results (500 events, maxConcurrency=20) === + + at Object. (src/__tests__/load.test.ts:108:15) + + console.log + Total processing time: 443ms + + at Object. (src/__tests__/load.test.ts:109:15) + + console.log + Throughput: 1128.67 events/second + + at Object. (src/__tests__/load.test.ts:110:15) + + console.log + Average latency per event: 0.89ms + + at Object. (src/__tests__/load.test.ts:111:15) + +ts-jest[ts-compiler] (WARN) src/services/providers/provider-health.test.ts:15:56 - +error TS2345: Argument of type '{}' is not assignable to parameter of type 'DiscordConfig'. + Type '{}' is missing the following properties from type 'DiscordConfig': webhookUrl, webhookId + +15 const provider = new DiscordNotificationProvider({}); +   ~~ +src/services/providers/provider-health.test.ts:26:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +26 const provider = new DiscordNotificationProvider({ +   ~ +27 webhookUrl: 'https://discord.com/api/webhooks/123/my-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +28 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +src/services/providers/provider-health.test.ts:42:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +42 const provider = new DiscordNotificationProvider({ +   ~ +43 webhookUrl: 'https://discord.com/api/webhooks/123456789/my-super-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +44 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +PASS src/services/providers/provider-health.test.ts + ● Console + + console.log + 2026-10-16T12:12:47.819Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/__tests__/query-performance.test.ts + ● Test suite failed to run + + Cannot find module 'vitest' from 'src/__tests__/query-performance.test.ts' + + > 1 | import { describe, it, expect } +from "vitest"; // if the repo uses jest, DELETE this line +  | ^ +  2 | import { readFileSync, writeFileSync, mkdirSync } from +"node:fs"; +  3 | import { dirname, resolve } from "node:path"; +  4 | import { performance } from "node:perf_hooks"; + + at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/index.js:1031:11) + at Object. (src/__tests__/query-performance.test.ts:1:1) + +PASS src/utils/batch-validator.test.ts +PASS src/utils/cors-validator.test.ts +PASS src/services/dead-letter-queue.test.ts + ● Console + + console.log + 2026-10-16T12:12:48.820Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.558Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.561Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.566Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.583Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.643Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.645Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T12:12:54.593Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.652Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:49.660Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.465Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.466Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.468Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.483Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.512Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"email","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.513Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T12:12:55.484Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.532Z info: Dead-letter notification requeued +{"requestId":"req-2","id":1,"notificationId":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:50.538Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/webhook-replay-cache.test.ts +PASS src/services/notification-import-service.test.ts + ● Console + + console.log + 2026-10-16T12:12:51.486Z info: Bulk notification import complete +{"imported":2,"skipped":0,"total":2,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:51.512Z info: Bulk notification import complete +{"imported":1,"skipped":2,"total":3,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:51.518Z info: Bulk notification import complete +{"imported":1,"skipped":1,"total":2,"format":"csv"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-provenance.test.ts + ● Console + + console.log + 2026-10-16T12:12:51.648Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:51.668Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:51.670Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:51.676Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:51.693Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/integration.test.ts + ● Console + + console.log + 2026-10-16T12:12:52.349Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":2,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/provider-registry.test.ts + ● Console + + console.log + 2026-10-16T12:12:52.624Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock +Provider","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:52.648Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing +Mock","capabilities":[]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:52.653Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial +Mock","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-quarantine.test.ts +PASS src/utils/json-logger-formatter.test.ts +PASS src/utils/app-version.test.ts +PASS src/services/notification-metrics-store.test.ts +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +PASS src/database/query-performance.test.ts (6.818 s) + ● Console + + console.log + 2026-10-16T12:12:54.789Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792152774787.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:56.506Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:56.509Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:12:56.514Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792152774787.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:13:00.846Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/delivery-receipt-repository.test.ts +PASS src/api/request-size-limiter.test.ts +PASS src/config/schema-validator.test.ts (9.573 s) +PASS src/services/dead-letter-manager.test.ts +PASS src/services/notification-metrics-runner.test.ts +PASS src/utils/pagination.test.ts (8.238 s) +PASS src/providers/provider-capabilities.test.ts +PASS src/utils/payload-compression.test.ts + ● Console + + console.log + 2026-10-16T12:13:13.415Z info: Payload compressed +{"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:13:13.489Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:13:13.493Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/metadata-validator.test.ts +PASS src/api/cors-validator.test.ts +PASS src/services/indexing-reconciliation-engine.test.ts +PASS src/middleware/request-id.test.ts +PASS src/services/job-monitor.test.ts + ● Console + + console.log + 2026-10-16T12:13:15.693Z error: Job monitor: job failed +{"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/config-snapshot.test.ts +PASS src/utils/log-level-resolver.test.ts +PASS src/middleware/security-headers.test.ts +PASS src/utils/notification-version.test.ts +PASS src/services/notification-health-monitor.test.ts + ● Console + + console.log + 2026-10-16T12:13:16.750Z info: NotificationHealthMonitor started {"intervalMs":30000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:13:16.782Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + +Summary of all failing tests +FAIL src/__tests__/notification-flow-e2e.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\__tests__\notification-flow-e2e.test.ts:302 + it(, async) { } + ^ + + SyntaxError: Unexpected token ',' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/api/events-server.test.ts + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792065998377} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792152399463, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792065998377} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792152399463, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "ba669e8f"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/services/webhook-verifier.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\services\webhook-verifier.test.ts:458 + expect(outcome) { }, : .authenticated + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (28.714 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/tests/template-api-integration.test.ts + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/retry-deduplication.test.ts (10.592 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event from ingestion to notification +generation + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should handle multiple events in sequence + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing id + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with invalid ledger + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching wildcard filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching specific filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should reject event not matching filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should extract event name from topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should prevent duplicate notifications + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should allow same event from different contracts + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should handle notification failure gracefully + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should continue processing after individual event failure + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should process events quickly (< 100ms +per event) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should handle registry size limits + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should clean up resources properly + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should execute in under 2 seconds + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should make zero external network calls + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should run deterministically (same input = same +output) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/discord-notification.test.ts (5.614 s) + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:125:14) + at Object. (src/services/archive.test.ts:460:21) + + ● handleArchiveRequest › GET /api/archive/:id returns 404 for unknown id + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:82:14) + at Object. (src/services/archive.test.ts:471:21) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:82:14) + at Object. (src/services/archive.test.ts:497:21) + + ● handleArchiveRequest › POST /api/archive/run returns 503 when service not provided + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:49:14) + at Object. (src/services/archive.test.ts:506:47) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:58:14) + at Object. (src/services/archive.test.ts:517:21) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › classifies an AbortError as a timeout + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:431:36) + + ● sendWebhookWithRetry › classifyWebhookFailure › classifies a TimeoutError as a timeout + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:438:36) + + ● sendWebhookWithRetry › classifyWebhookFailure › classifies a generic error as a network failure (not a timeout) + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:442:36) + + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   69 | // own reason; everything else is a network-level failure. +  70 | if (error) { + > 71 | return isWebhookTimeoutError(error) ? +'timeout' : 'network'; +  | ^ +  72 | } +  73 | +  74 | if (!response || response.ok) { + + at classifyWebhookFailure (src/services/webhook-retry-helper.ts:71:33) + at Object. (src/services/webhook-retry-helper.test.ts:448:36) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (10.9 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/tests/notification-scheduler-refactored.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\tests\notification-scheduler-refactored.test.ts:271 + const pastLock = notification_fixture_builder_1.NotificationFixtureBuilder.dates.past(1000); + ^ + + SyntaxError: Identifier 'pastLock' has already been declared + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/discord-notification-refactored.test.ts (77.206 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/api/queue-visibility.test.ts (33.364 s) + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + Server is not running. + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/concurrent-event-processing.test.ts (17.795 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 1) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/__tests__/stress.test.ts (193.838 s) + ● Stress Test Suite: Notification Processing › 6. Discord Notification Service Under Load › should handle 500 +Discord notifications with deduplication + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: 0 + +   631 | console.log(` Duplicates Skipped: +${metrics.duplicatesSkipped}`); +  632 | + > 633 | +expect(successCount.value).toBe(eventCount); +  | ^ +  634 | expect(report.throughput).toBeGreaterThan(50); +  635 | }, 120000); +  636 | }); + + at Object. (src/__tests__/stress.test.ts:633:34) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   637 | +  638 | describe('7. Error Recovery Under Stress', () => { + > 639 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  640 | const eventCount = 1000; +  641 | const processedIds: string[] = []; +  642 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:639:5 + at src/__tests__/stress.test.ts:638:3 + at Object. (src/__tests__/stress.test.ts:37:1) + +FAIL src/services/execution-metrics.test.ts (8.106 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "9b0c3a1a"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "b1ef8db2-ed61-45e0-a888-0e572dcd4990", +"eventId": "event-1", "requestId": "b1ef8db2-ed61-45e0-a888-0e572dcd4990"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/api/queue-operational-metrics.test.ts (18.032 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + + ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — +Dedicated Endpoint › supports aliases /api/schedule/queue-metrics and /api/notifications/queue-metrics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let testDbPath: string; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | testDbIndex++; +  77 | testDbPath = path.join( +  78 | __dirname, + + at src/api/queue-operational-metrics.test.ts:75:13 + at Object. (src/api/queue-operational-metrics.test.ts:68:9) + +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:79:22) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:89:22) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:98:7) + + ● WebhookDeliveryService › successful delivery (2xx) › passes the payload and options to sendWebhook + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:109:7) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:138:7) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:167:7) + + ● WebhookDeliveryService › request timeout (AbortError) › returns success: false when the request times out + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:183:22) + + ● WebhookDeliveryService › request timeout (AbortError) › logs a warn for timeouts so the caller knows to retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:193:7) + + ● WebhookDeliveryService › request timeout (AbortError) › includes the configured timeout in the errorReason + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:205:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › returns success: false on a generic network +error + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:217:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › logs an error for non-timeout network failures + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:226:7) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › handles non-Error thrown values gracefully + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:237:22) + + ● WebhookDeliveryService › default options › uses the instance-level timeout when none provided per-call + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:251:7) + + ● WebhookDeliveryService › default options › merges instance headers with per-call headers + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:264:7) + + ● WebhookDeliveryService › failure classification › labels a request timeout as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:282:22) + + ● WebhookDeliveryService › failure classification › labels a TimeoutError as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:292:22) + + ● WebhookDeliveryService › failure classification › labels a generic network error as "network" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:300:22) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:309:22) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:317:22) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:325:22) + +FAIL src/services/scheduled-notification-queue.test.ts (11.285 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   109 | await Promise.resolve(); +  110 | await Promise.resolve(); + > 111 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  112 | +  113 | queue.stop(); +  114 | }); + + at Object. (src/services/notification-retry-queue.test.ts:111:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   128 | await Promise.resolve(); +  129 | + > 130 | expect(queue.size()).toBe(0); +  | ^ +  131 | queue.stop(); +  132 | }); +  133 | + + at Object. (src/services/notification-retry-queue.test.ts:130:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4493.069588561699, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:10:58.410Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   146 | await Promise.resolve(); +  147 | + > 148 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  149 | 'Retry succeeded', +  150 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  151 | ); + + at Object. (src/services/notification-retry-queue.test.ts:148:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   170 | await Promise.resolve(); +  171 | await Promise.resolve(); + > 172 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  173 | +  174 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  175 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:172:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   198 | await Promise.resolve(); +  199 | + > 200 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  201 | 'Retry failed, scheduling next attempt', +  202 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  203 | ); + + at Object. (src/services/notification-retry-queue.test.ts:200:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   225 | jest.advanceTimersByTime(100); +  226 | await flush(); + > 227 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  228 | +  229 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  230 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:227:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   257 | await Promise.resolve(); +  258 | + > 259 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  260 | 'Notification permanently failed after max retries', +  261 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  262 | ); + + at Object. (src/services/notification-retry-queue.test.ts:259:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue.test.ts:299:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   349 | +  350 | // The notification fn was called exactly 3 times + > 351 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  352 | +  353 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  354 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:351:30) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (53.723 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   168 | +  169 | describe('fan-out delivery', () => { + > 170 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  171 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  172 | +  173 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   177 | }); +  178 | + > 179 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  180 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  181 | +  182 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:179:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   193 | +  194 | describe('channel-specific payloads', () => { + > 195 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  196 | await deliverToAllChannels( +  197 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  198 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   209 | }); +  210 | + > 211 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  212 | await deliverToAllChannels( +  213 | createMockEvent({ id: 'event-system', type: +'system' }) +  214 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:211:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   224 | +  225 | describe('partial delivery failure', () => { + > 226 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  227 | +router.setHandler(channels[1].config.webhookUrl, () => +  228 | failedFetch(500, 'Internal Server Error') +  229 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   234 | }); +  235 | + > 236 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  237 | +router.setHandler(channels[1].config.webhookUrl, () => +  238 | failedFetch(429, 'Too Many Requests', +'rate limited') +  239 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:236:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   259 | }); +  260 | + > 261 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  262 | +router.setHandler(channels[2].config.webhookUrl, () => { +  263 | throw new Error('ECONNRESET'); +  264 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:261:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   288 | }; +  289 | + > 290 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  291 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  292 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  293 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:290:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   314 | }); +  315 | + > 316 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  317 | // 'audit' never recovers. +  318 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  319 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:316:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   339 | }); +  340 | + > 341 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  342 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  343 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  344 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:341:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   86 | await Promise.resolve(); +  87 | await Promise.resolve(); + > 88 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  89 | +  90 | queue.stop(); +  91 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:88:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   106 | await Promise.resolve(); +  107 | + > 108 | expect(queue.size()).toBe(0); +  | ^ +  109 | queue.stop(); +  110 | }); +  111 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:108:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 5610.902651429087, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:11:12.976Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   131 | await Promise.resolve(); +  132 | + > 133 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  134 | 'Retry succeeded', +  135 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  136 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:133:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   156 | await Promise.resolve(); +  157 | await Promise.resolve(); + > 158 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  159 | +  160 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  161 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:158:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   189 | await Promise.resolve(); +  190 | + > 191 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  192 | 'Retry failed, scheduling next attempt', +  193 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  194 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:191:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   218 | jest.advanceTimersByTime(100); +  219 | await flush(); + > 220 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  221 | +  222 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  223 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:220:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   255 | await Promise.resolve(); +  256 | + > 257 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  258 | 'Notification permanently failed after max retries', +  259 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  260 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:257:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:299:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   326 | } +  327 | + > 328 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  329 | queue.stop(); +  330 | }); +  331 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:328:30) + +FAIL src/api/execution-metrics-api.test.ts (7.419 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/api/template-routes.test.ts + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:85:33) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:107:33) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:117:33) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:137:33) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:153:33) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › GET /api/templates › accepts a valid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:5) + at Object. (src/api/template-routes.test.ts:175:7) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:201:33) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:215:33) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:226:33) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:237:33) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:247:33) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:261:33) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/__tests__/retry-scheduler.integration.test.ts (10.595 s) + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/tests/events-server.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\tests\events-server.test.ts:11 + headers ? : Record; + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › isDuplicate › returns true for a fingerprint that has been marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:41:9) + + ● NotificationDeduplicator › isDuplicate › returns false for a different fingerprint after another is marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:47:9) + + ● NotificationDeduplicator › markSent › increments size after marking a new fingerprint + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:55:9) + + ● NotificationDeduplicator › markSent › does not increment size when marking the same fingerprint twice + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:61:9) + + ● NotificationDeduplicator › markSent › evicts the oldest entry when the cache is full + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:68:9) + + ● NotificationDeduplicator › markSent › logs a warning when evicting due to full cache + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:83:9) + + ● NotificationDeduplicator › markSent › does not evict when cache has room + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:95:9) + + ● NotificationDeduplicator › time window and metrics › allows the same fingerprint after the deduplication window +expires + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:109:9) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:120:9) + + ● NotificationDeduplicator › time window and metrics › counts evicted and expired entries in metrics + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:140:9) + + ● NotificationDeduplicator › size › returns the correct count after multiple unique fingerprints + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:163:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › prevents a second notification for the +same event on re-poll + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:175:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › allows notifications for unique events +with the same contract + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:182:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › treats same event id on different +contracts as distinct + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:190:9) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › GET /api/archive › accepts a request with valid filters + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:125:14) + at Object. (src/api/archive-api.test.ts:101:7) + + ● archive-api validation › GET /api/archive › accepts a request with no filters at all + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:125:14) + at Object. (src/api/archive-api.test.ts:113:7) + + ● archive-api validation › GET /api/archive/:id › accepts a valid positive integer id + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:82:14) + at Object. (src/api/archive-api.test.ts:145:7) + + ● archive-api validation › POST /api/archive/run › returns 503 when the archive service is not configured + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:49:14) + at Object. (src/api/archive-api.test.ts:157:33) + + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + TypeError: res.setHeader is not a function + +   118 |  */ +  119 | export function sendJson(res: +http.ServerResponse, statusCode: number, body: +unknown): void { + > 120 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  121 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  122 | res.end(JSON.stringify(body)); +  123 | } + + at sendJson (src/utils/response.ts:120:7) + at sendErr (src/utils/response.ts:155:3) + at handleArchiveRequest (src/api/archive-api.ts:58:14) + at Object. (src/api/archive-api.test.ts:167:7) + +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/tests/payload-integrity.test.ts (5.697 s) + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/tests/api-versioning.test.ts + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/__tests__/database-recovery.test.ts (6.336 s) + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   29 | }); +  30 | + > 31 | test('recovers transparently when the handle is closed +mid-flight', async () => { +  | ^ +  32 | const db = new Database(testDbPath, { +maxReconnectAttempts: 3, reconnectBaseDelayMs: 10 }); +  33 | await db.initialize(); +  34 | await db.run('CREATE TABLE t (id INTEGER PRIMARY KEY, v +TEXT)'); + + at src/__tests__/database-recovery.test.ts:31:3 + at Object. (src/__tests__/database-recovery.test.ts:18:1) + + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + +FAIL src/__tests__/scheduler-concurrency.test.ts (23.007 s) + ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   123 | }); +  124 | + > 125 | test('concurrency larger than the batch is clamped to the batch +size', async () => { +  | ^ +  126 | await seedDueNotifications(2); +  127 | await sleep(1300); +  128 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:125:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +FAIL src/__tests__/query-performance.test.ts + ● Test suite failed to run + + Cannot find module 'vitest' from 'src/__tests__/query-performance.test.ts' + + > 1 | import { describe, it, expect } +from "vitest"; // if the repo uses jest, DELETE this line +  | ^ +  2 | import { readFileSync, writeFileSync, mkdirSync } from +"node:fs"; +  3 | import { dirname, resolve } from "node:path"; +  4 | import { performance } from "node:perf_hooks"; + + at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/index.js:1031:11) + at Object. (src/__tests__/query-performance.test.ts:1:1) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + + +Test Suites: 52 failed, 1 skipped, 100 passed, 152 of 153 total +Tests: 308 failed, 4 skipped, 2106 passed, 2418 total +Snapshots: 0 total +Time: 417.448 s +Ran all test suites. diff --git a/_jest2.txt b/_jest2.txt new file mode 100644 index 00000000..f4e2ed0d --- /dev/null +++ b/_jest2.txt @@ -0,0 +1,22036 @@ +node : FAIL src/services/discord-notification-refactored.test.ts (87.284 s) +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=2 --wor ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/servic...t.ts (87.284 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (50.827 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   168 | +  169 | describe('fan-out delivery', () => { + > 170 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  171 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  172 | +  173 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   177 | }); +  178 | + > 179 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  180 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  181 | +  182 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:179:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   193 | +  194 | describe('channel-specific payloads', () => { + > 195 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  196 | await deliverToAllChannels( +  197 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  198 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   209 | }); +  210 | + > 211 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  212 | await deliverToAllChannels( +  213 | createMockEvent({ id: 'event-system', type: +'system' }) +  214 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:211:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   224 | +  225 | describe('partial delivery failure', () => { + > 226 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  227 | +router.setHandler(channels[1].config.webhookUrl, () => +  228 | failedFetch(500, 'Internal Server Error') +  229 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   234 | }); +  235 | + > 236 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  237 | +router.setHandler(channels[1].config.webhookUrl, () => +  238 | failedFetch(429, 'Too Many Requests', +'rate limited') +  239 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:236:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   259 | }); +  260 | + > 261 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  262 | +router.setHandler(channels[2].config.webhookUrl, () => { +  263 | throw new Error('ECONNRESET'); +  264 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:261:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   288 | }; +  289 | + > 290 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  291 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  292 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  293 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:290:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   314 | }); +  315 | + > 316 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  317 | // 'audit' never recovers. +  318 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  319 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:316:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   339 | }); +  340 | + > 341 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  342 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  343 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  344 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:341:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + +PASS src/api/queue-visibility.test.ts (18.04 s) + ● Console + + console.log + 2026-10-16T12:20:26.550Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.319Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.323Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.330Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.368Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.396Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.410Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.486Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:27.494Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.221Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.223Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.225Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.241Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.248Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.973Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.974Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.976Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:28.986Z info: Handling GET /api/schedule/queue +{"requestId":"9ad13843","correlationId":"68d6d018-7f41-4b94-963c-59b5d68de415","count":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.014Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.019Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.736Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.738Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.740Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.758Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.763Z info: Handling GET /api/schedule/queue +{"requestId":"1a3c0e9f","correlationId":"01f05022-2523-4ca0-ba6c-53b557b65347","count":1,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.774Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:29.779Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.504Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.507Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.509Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.525Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.540Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.558Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.589Z warn: Notification moved to dead letter queue +{"id":2,"notificationType":"discord","failureReason":"oops"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.592Z info: Notification marked for retry or failed +{"id":2,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.605Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.613Z info: Handling GET /api/schedule/queue +{"requestId":"0a28a634","correlationId":"a1c7e524-a0f7-44e8-b95b-f448ce1f93d4","count":1,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.619Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:30.626Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.342Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.343Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.344Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.362Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.381Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.395Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.409Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.422Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.429Z info: Handling GET /api/schedule/queue +{"requestId":"5ffa7302","correlationId":"ee9e37c1-202f-488f-b8cd-39619ba5de48","count":3,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.433Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:31.440Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:32.814Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:32.815Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:32.816Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:32.830Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:32.835Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.535Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.537Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.539Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.554Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.560Z info: Handling GET /api/schedule/queue +{"requestId":"f8913365","correlationId":"31cbec20-3d56-41be-8161-109cad1acfae","count":1,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.564Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:33.569Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:34.286Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:34.288Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:34.290Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:34.302Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:34.309Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.036Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.039Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.040Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.049Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.064Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.073Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.076Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM +dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.077Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibi +lity.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.090Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.197Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.200Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.874Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.876Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.877Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.884Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.896Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.901Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.903Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM +dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:35.905Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibi +lity.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (19.154 s) + ● Console + + console.log + 2026-10-16T12:20:38.572Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.595Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.597Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.599Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.660Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.662Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.664Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.666Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.866Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.868Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.870Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:38.871Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.027Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.031Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.034Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.039Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.980Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.986Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.991Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:44.995Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:45.108Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:45.112Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:45.117Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:45.121Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:53.829Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.275Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.280Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.283Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.918Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.950Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.958Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:54.961Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/__tests__/scheduler-concurrency.test.ts (11.803 s) + ● Console + + console.log + 2026-10-16T12:20:56.754Z info: Connected to SQLite database +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.734Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.744Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.764Z info: Database initialized successfully +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.781Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.845Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.859Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.897Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.904Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.926Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.929Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:57.976Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.312Z info: Fetched and locked pending notifications +{"requestId":"544190d9","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.315Z info: Processing batch of scheduled notifications +{"requestId":"544190d9","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.321Z info: Processing scheduled notification +{"requestId":"544190d9","id":1,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.433Z info: Notification marked as completed {"requestId":"544190d9","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.454Z info: Notification delivered successfully +{"requestId":"544190d9","id":1,"type":"discord","durationMs":95} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.458Z info: Processing scheduled notification +{"requestId":"544190d9","id":2,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.568Z info: Notification marked as completed {"requestId":"544190d9","id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.590Z info: Notification delivered successfully +{"requestId":"544190d9","id":2,"type":"discord","durationMs":93} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.593Z info: Processing scheduled notification +{"requestId":"544190d9","id":3,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.701Z info: Notification marked as completed {"requestId":"544190d9","id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.722Z info: Notification delivered successfully +{"requestId":"544190d9","id":3,"type":"discord","durationMs":96} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.726Z info: Processing scheduled notification +{"requestId":"544190d9","id":4,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.827Z info: Notification marked as completed {"requestId":"544190d9","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.843Z info: Notification delivered successfully +{"requestId":"544190d9","id":4,"type":"discord","durationMs":85} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:20:59.847Z info: Scheduler batch complete +{"requestId":"544190d9","processorId":"test-uuid-0001","count":4,"durationMs":555} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.856Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.906Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.927Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.929Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.946Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.949Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.973Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:20:59.978Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:00.006Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:00.009Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:00.037Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:00.043Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:00.065Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.411Z info: Fetched and locked pending notifications +{"requestId":"2caafd41","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.415Z info: Processing batch of scheduled notifications +{"requestId":"2caafd41","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.420Z info: Processing scheduled notification +{"requestId":"2caafd41","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:01.423Z info: Processing scheduled notification +{"requestId":"2caafd41","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:01.425Z info: Processing scheduled notification +{"requestId":"2caafd41","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:21:01.546Z info: Notification marked as completed {"requestId":"2caafd41","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:01.550Z info: Notification marked as completed {"requestId":"2caafd41","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:01.554Z info: Notification marked as completed {"requestId":"2caafd41","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:21:01.572Z info: Notification delivered successfully +{"requestId":"2caafd41","id":6,"type":"discord","durationMs":89} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:01.577Z info: Processing scheduled notification +{"requestId":"2caafd41","id":8,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:01.596Z info: Notification delivered successfully +{"requestId":"2caafd41","id":5,"type":"discord","durationMs":89} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:01.601Z info: Processing scheduled notification +{"requestId":"2caafd41","id":9,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:01.606Z info: Notification delivered successfully +{"requestId":"2caafd41","id":7,"type":"discord","durationMs":89} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:21:01.609Z info: Processing scheduled notification +{"requestId":"2caafd41","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:21:01.676Z info: Notification marked as completed {"requestId":"2caafd41","id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:01.692Z info: Notification delivered successfully +{"requestId":"2caafd41","id":8,"type":"discord","durationMs":85} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:01.723Z info: Notification marked as completed {"requestId":"2caafd41","id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:01.728Z info: Notification marked as completed {"requestId":"2caafd41","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:21:01.760Z info: Notification delivered successfully +{"requestId":"2caafd41","id":9,"type":"discord","durationMs":94} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:01.765Z info: Notification delivered successfully +{"requestId":"2caafd41","id":10,"type":"discord","durationMs":86} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T12:21:01.767Z info: Scheduler batch complete +{"requestId":"2caafd41","processorId":"test-uuid-0002","count":6,"durationMs":382} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.774Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.816Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.848Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.853Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:01.873Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:03.561Z info: Fetched and locked pending notifications +{"requestId":"83a867e1","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:03.566Z info: Processing batch of scheduled notifications +{"requestId":"83a867e1","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:03.568Z info: Processing scheduled notification +{"requestId":"83a867e1","id":11,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:03.571Z info: Processing scheduled notification +{"requestId":"83a867e1","id":12,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:05.137Z info: Notification marked as completed {"requestId":"83a867e1","id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:05.140Z info: Notification marked as completed {"requestId":"83a867e1","id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:07.343Z info: Notification delivered successfully +{"requestId":"83a867e1","id":11,"type":"discord","durationMs":93} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:21:07.347Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:07.466Z info: Notification delivered successfully +{"requestId":"83a867e1","id":12,"type":"discord","durationMs":92} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T12:21:07.467Z info: Scheduler batch complete +{"requestId":"83a867e1","processorId":"test-uuid-0003","count":2,"durationMs":4287} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:07.470Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   123 | }); +  124 | + > 125 | test('concurrency larger than the batch is clamped to the batch +size', async () => { +  | ^ +  126 | await seedDueNotifications(2); +  127 | await sleep(1300); +  128 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:125:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +FAIL src/__tests__/stress.test.ts (209.354 s) + ● Console + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: 1,000 Concurrent Events + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 486ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 2057.61 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 11.08ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 12.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 174.17 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 180.20 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 6.02 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 300.50 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 299.75 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: -0.75 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: 5,000 Events Sustained Load + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 1956ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 2556.24 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 28.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 9.97ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 10.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 28.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 183.37 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 179.83 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: -3.54 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 175.49 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 187.53 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 12.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: 10,000 Events - System Stability + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 5119ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 1953.51 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 42.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 10.11ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 9.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 19.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 33.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 182.00 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 189.28 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 7.29 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 189.06 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 148.44 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: -40.62 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + Sending first burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:355:15) + + console.log + Waiting idle period (2 seconds)... + + at Object. (src/__tests__/stress.test.ts:372:15) + + console.log + Sending second burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:376:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Burst Traffic (2x2000 events) + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 3541ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 1129.62 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 10.03ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 10.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 192.33 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 216.02 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 23.69 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 152.41 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 153.27 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 0.85 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + First burst time: 810ms + + at Object. (src/__tests__/stress.test.ts:405:15) + + console.log + Second burst time: 718ms + + at Object. (src/__tests__/stress.test.ts:406:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Priority Queue Under Load + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 1036ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 1930.50 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 19.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 10.83ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 10.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 218.45 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 231.80 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 13.35 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 154.95 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 164.32 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 9.38 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + Priority Analysis: + + at Object. (src/__tests__/stress.test.ts:491:15) + + console.log + High-priority in first 500: 500 / 500 (100.00%) + + at Object. (src/__tests__/stress.test.ts:492:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Deduplication Test (3000 events, 1500 unique) + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 3000 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 1500 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 1527ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 1964.64 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 3.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 10.81ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 11.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 233.89 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 198.94 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: -34.94 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 165.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 175.33 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 10.29 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + Deduplication Efficiency: + + at Object. (src/__tests__/stress.test.ts:562:15) + + console.log + Total Enqueued: 3000 + + at Object. (src/__tests__/stress.test.ts:563:15) + + console.log + Unique Events: 1500 + + at Object. (src/__tests__/stress.test.ts:564:15) + + console.log + Actually Processed: 1500 + + at Object. (src/__tests__/stress.test.ts:565:15) + + console.log + Duplicates Blocked: 1500 + + at Object. (src/__tests__/stress.test.ts:566:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + + console.log + STRESS TEST REPORT: Discord Notification Service Load + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + Events Processed: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Success Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Failure Count: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Total Time: 3206ms + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Throughput: 155.96 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + Min: 3060.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Max: 3141.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Avg: 3109.38ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + P50 (Median): 3108.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P95: 3138.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P99: 3141.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + + console.log + Before - Heap: 200.54 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + After - Heap: 208.27 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + Delta - Heap: 7.73 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Before - RSS: 178.07 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + After - RSS: 192.12 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + Delta - RSS: 14.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + + console.log + + Discord Service Metrics: + + at Object. (src/__tests__/stress.test.ts:629:15) + + console.log + Total Sent: undefined + + at Object. (src/__tests__/stress.test.ts:630:15) + + console.log + Duplicates Skipped: undefined + + at Object. (src/__tests__/stress.test.ts:631:15) + + ● Stress Test Suite: Notification Processing › 6. Discord Notification Service Under Load › should handle 500 +Discord notifications with deduplication + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: 0 + +   631 | console.log(` Duplicates Skipped: +${metrics.duplicatesSkipped}`); +  632 | + > 633 | +expect(successCount.value).toBe(eventCount); +  | ^ +  634 | expect(report.throughput).toBeGreaterThan(50); +  635 | }, 120000); +  636 | }); + + at Object. (src/__tests__/stress.test.ts:633:34) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   637 | +  638 | describe('7. Error Recovery Under Stress', () => { + > 639 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  640 | const eventCount = 1000; +  641 | const processedIds: string[] = []; +  642 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:639:5 + at src/__tests__/stress.test.ts:638:3 + at Object. (src/__tests__/stress.test.ts:37:1) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + ================================================================================". + +   93 | +  94 | // Log to console for visibility + > 95 | console.log('\n' + +'='.repeat(80)); +  | ^ +  96 | console.log(`STRESS TEST REPORT: ${testName}`); +  97 | console.log('='.repeat(80)); +  98 | console.log(`Events Processed: ${eventCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:95:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "STRESS TEST REPORT: Error Recovery Under Load". + +   94 | // Log to console for visibility +  95 | console.log('\n' + +'='.repeat(80)); + > 96 | console.log(`STRESS TEST REPORT: +${testName}`); +  | ^ +  97 | console.log('='.repeat(80)); +  98 | console.log(`Events Processed: ${eventCount}`); +  99 | console.log(`Success Count: ${successCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================". + +   95 | console.log('\n' + +'='.repeat(80)); +  96 | console.log(`STRESS TEST REPORT: ${testName}`); + > 97 | +console.log('='.repeat(80)); +  | ^ +  98 | console.log(`Events Processed: ${eventCount}`); +  99 | console.log(`Success Count: ${successCount}`); +  100 | console.log(`Failure Count: ${failureCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Events Processed: 1000". + +   96 | console.log(`STRESS TEST REPORT: ${testName}`); +  97 | console.log('='.repeat(80)); + > 98 | console.log(`Events Processed: +${eventCount}`); +  | ^ +  99 | console.log(`Success Count: ${successCount}`); +  100 | console.log(`Failure Count: ${failureCount}`); +  101 | console.log(`Total Time: ${totalTimeMs}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Success Count: 900". + +   97 | console.log('='.repeat(80)); +  98 | console.log(`Events Processed: ${eventCount}`); + > 99 | console.log(`Success Count: +${successCount}`); +  | ^ +  100 | console.log(`Failure Count: ${failureCount}`); +  101 | console.log(`Total Time: ${totalTimeMs}ms`); +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Failure Count: 0". + +   98 | console.log(`Events Processed: ${eventCount}`); +  99 | console.log(`Success Count: ${successCount}`); + > 100 | console.log(`Failure Count: +${failureCount}`); +  | ^ +  101 | console.log(`Total Time: ${totalTimeMs}ms`); +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  103 | console.log(`\nLatency Statistics:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Total Time: 181018ms". + +   99 | console.log(`Success Count: ${successCount}`); +  100 | console.log(`Failure Count: ${failureCount}`); + > 101 | console.log(`Total Time: +${totalTimeMs}ms`); +  | ^ +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  103 | console.log(`\nLatency Statistics:`); +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Throughput: 5.52 events/second". + +   100 | console.log(`Failure Count: ${failureCount}`); +  101 | console.log(`Total Time: ${totalTimeMs}ms`); + > 102 | console.log(`Throughput: +${throughput.toFixed(2)} events/second`); +  | ^ +  103 | console.log(`\nLatency Statistics:`); +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Latency Statistics:". + +   101 | console.log(`Total Time: ${totalTimeMs}ms`); +  102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + > 103 | console.log(`\nLatency Statistics:`); +  | ^ +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Min: 0.00ms". + +   102 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  103 | console.log(`\nLatency Statistics:`); + > 104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  | ^ +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Max: 22.00ms". + +   103 | console.log(`\nLatency Statistics:`); +  104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + > 105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  | ^ +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Avg: 11.75ms". + +   104 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + > 106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  | ^ +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); +  109 | console.log(` P99: ${p99.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P50 (Median): 12.00ms". + +   105 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + > 107 | console.log(` P50 (Median): +${p50.toFixed(2)}ms`); +  | ^ +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); +  109 | console.log(` P99: ${p99.toFixed(2)}ms`); +  110 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P95: 17.00ms". + +   106 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + > 108 | console.log(` P95: +${p95.toFixed(2)}ms`); +  | ^ +  109 | console.log(` P99: ${p99.toFixed(2)}ms`); +  110 | +  111 | if (memoryUsage) { + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P99: 21.00ms". + +   107 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  108 | console.log(` P95: ${p95.toFixed(2)}ms`); + > 109 | console.log(` P99: +${p99.toFixed(2)}ms`); +  | ^ +  110 | +  111 | if (memoryUsage) { +  112 | console.log(`\nMemory Usage:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Memory Usage:". + +   110 | +  111 | if (memoryUsage) { + > 112 | console.log(`\nMemory Usage:`); +  | ^ +  113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:112:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - Heap: 209.69 MB". + +   111 | if (memoryUsage) { +  112 | console.log(`\nMemory Usage:`); + > 113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  | ^ +  114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - Heap: 185.00 MB". + +   112 | console.log(`\nMemory Usage:`); +  113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); + > 114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  | ^ +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - Heap: -24.69 MB". + +   113 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); + > 115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  | ^ +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - RSS: 192.99 MB". + +   114 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + > 116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  | ^ +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  119 | } + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - RSS: 32.70 MB". + +   115 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + > 117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  | ^ +  118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  119 | } +  120 | console.log('='.repeat(80) + +'\n'); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - RSS: -160.29 MB". + +   116 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  117 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + > 118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  | ^ +  119 | } +  120 | console.log('='.repeat(80) + +'\n'); +  121 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================ + ". + +   118 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  119 | } + > 120 | console.log('='.repeat(80) ++ '\n'); +  | ^ +  121 | +  122 | return report; +  123 | }; + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:120:13) + at Object. (src/__tests__/stress.test.ts:697:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Error Recovery Statistics:". + +   706 | ); +  707 | + > 708 | console.log(`\nError Recovery +Statistics:`); +  | ^ +  709 | console.log(` Initial Failures: ${retryCount}`); +  710 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  711 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:708:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Initial Failures: 300". + +   707 | +  708 | console.log(`\nError Recovery Statistics:`); + > 709 | console.log(` Initial Failures: +${retryCount}`); +  | ^ +  710 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  711 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  712 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:709:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Successful Retries: 200". + +   708 | console.log(`\nError Recovery Statistics:`); +  709 | console.log(` Initial Failures: ${retryCount}`); + > 710 | console.log(` Successful Retries: ${processedIds.length +- (eventCount - retryCount)}`); +  | ^ +  711 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  712 | +  713 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:710:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Final Success Rate: 90.00%". + +   709 | console.log(` Initial Failures: ${retryCount}`); +  710 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); + > 711 | console.log(` Final Success Rate: +${((processedIds.length / eventCount) * 100).toFixed(2)}%`); +  | ^ +  712 | +  713 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success +  714 | expect(retryCount).toBeGreaterThan(0); // Verify +retries occurred + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:711:15) + +FAIL src/api/queue-operational-metrics.test.ts (21.512 s) + ● Console + + console.log + 2026-10-16T12:21:11.982Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153271942-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:13.200Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:13.426Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:13.663Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153271942-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:13.894Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:13.997Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153273990-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.882Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.884Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.891Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153273990-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.945Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.963Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.983Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:14.997Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153274993-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.880Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.884Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.894Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153274993-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.928Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.948Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.965Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:16.983Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153276979-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.058Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.061Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.063Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153276979-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.092Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.114Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.211Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:18.309Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153278269-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.327Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.329Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.331Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153278269-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.351Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.373Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.433Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.440Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.475Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:19.483Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153279481-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:20.456Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:20.459Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:20.462Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153279481-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:20.477Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:20.488Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153280483-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.241Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.243Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.245Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153280483-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.265Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.285Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.303Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.323Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"cdd411d4","correlationId":"c680e215-9233-416a-b05f-77109b8279a7","durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.332Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:21.340Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153281338-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:26.875Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:26.879Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:26.882Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operat +ional-metrics-1792153281338-8.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database +handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:26.915Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153286913-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:26.998Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153281338-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.001Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153281338-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.011Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.015Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.017Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153281338-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.716Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.728Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.732Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153286913-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.767Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:27.783Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153287780-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:28.572Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:28.576Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:28.579Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153287780-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:28.608Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:28.630Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:28.639Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153288634-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.377Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.379Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.380Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792153288634-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.401Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.413Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.428Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:29.432Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + + ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — +Dedicated Endpoint › supports aliases /api/schedule/queue-metrics and /api/notifications/queue-metrics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let testDbPath: string; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | testDbIndex++; +  77 | testDbPath = path.join( +  78 | __dirname, + + at src/api/queue-operational-metrics.test.ts:75:13 + at Object. (src/api/queue-operational-metrics.test.ts:68:9) + +FAIL src/services/scheduled-notification-queue.test.ts (9.543 s) + ● Console + + console.log + 2026-10-16T12:21:29.969Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.893Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.901Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.917Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.945Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.948Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.953Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.977Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.979Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:30.981Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:31.007Z info: Fetched and locked pending notifications +{"count":1,"processorId":"restarted-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:31.013Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:31.023Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.551Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.556Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.559Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.586Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.616Z info: Fetched and locked pending notifications +{"count":1,"processorId":"doomed-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.633Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.641Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.663Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.665Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.668Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.694Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.726Z info: Fetched and locked pending notifications +{"count":1,"processorId":"next-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.730Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:32.747Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.862Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.865Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.867Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.893Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.916Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.945Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.948Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.956Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.982Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.983Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.985Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:33.999Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.009Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.827Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.829Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.831Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.863Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.890Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.913Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.933Z info: Fetched and locked pending notifications +{"count":3,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.946Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:34.967Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:35.931Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:35.933Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:35.936Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:35.961Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:35.995Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:36.015Z info: Fetched and locked pending notifications +{"count":2,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:36.140Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:36.157Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:36.166Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.206Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.209Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.210Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.230Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.259Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.307Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.312Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:37.323Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.295Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.296Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.298Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.313Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.330Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.346Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:22:38.332Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.380Z info: Fetched due retries {"count":1,"processorId":"retry-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.383Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:38.391Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.118Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.121Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.123Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.139Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.155Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.187Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent outage"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.190Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:39.201Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (9.915 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● Console + + console.log + 2026-10-16T12:21:49.669Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:49.735Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:49.764Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:49.810Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:49.868Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/services/concurrent-event-processing.test.ts (21.241 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 3) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | let dbB: Database; +  129 | + > 130 | beforeEach(async () => { +  | ^ +  131 | jest.clearAllMocks(); +  132 | dbPath = path.join(os.tmpdir(), +`notify-concurrency-${process.pid}-${Date.now()}-${Math.random()}.db`); +  133 | dbA = await newDatabase(dbPath); + + at src/services/concurrent-event-processing.test.ts:130:3 + at Object. (src/services/concurrent-event-processing.test.ts:125:1) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + EBUSY: resource busy or locked, unlink +'C:\Users\NEWOWN~1\AppData\Local\Temp\notify-concurrency-13144-1792153303525-0.7452762566666198.db' + +   140 | await dbB.close(); +  141 | for (const suffix of ['', +'-journal', '-wal', '-shm']) { + > 142 | if (fs.existsSync(dbPath + suffix)) +fs.unlinkSync(dbPath + suffix); +  | ^ +  143 | } +  144 | }); +  145 | + + at Object. (src/services/concurrent-event-processing.test.ts:142:46) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + EBUSY: resource busy or locked, unlink +'C:\Users\NEWOWN~1\AppData\Local\Temp\notify-concurrency-13144-1792153308581-0.9283555795994962.db' + +   140 | await dbB.close(); +  141 | for (const suffix of ['', +'-journal', '-wal', '-shm']) { + > 142 | if (fs.existsSync(dbPath + suffix)) +fs.unlinkSync(dbPath + suffix); +  | ^ +  143 | } +  144 | }); +  145 | + + at Object. (src/services/concurrent-event-processing.test.ts:142:46) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/execution-metrics.test.ts (9.112 s) + ● Console + + console.log + 2026-10-16T12:21:51.285Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.544Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.549Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.560Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.579Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.612Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.649Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.675Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.681Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.795Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.767Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.769Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.772Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.792Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.823Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.839Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.867Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.894Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.909Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.940Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.990Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.992Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:54.039Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:54.040Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:54.046Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:54.160Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.802Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.806Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.808Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.828Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.860Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.874Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.912Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.928Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.961Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.996Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.019Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.061Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.090Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.107Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.135Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.181Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.203Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.246Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.298Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.301Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.355Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.358Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.369Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.488Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.455Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.457Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.459Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.474Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.507Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.524Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.555Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.573Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.632Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.633Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.638Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.750Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.624Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.625Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.627Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.632Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.742Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.729Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.731Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.732Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.767Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.819Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.862Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.866Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/services/retry-deduplication.test.ts (16.158 s) + ● Console + + console.log + 2026-10-16T12:21:50.206Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.013Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.015Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.025Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.041Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.073Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.116Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.118Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.160Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Final failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.162Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.168Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:51.279Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.491Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.492Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.494Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.521Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.551Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.558Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:52.669Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.647Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.649Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.651Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.671Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.706Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.758Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Second attempt failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.760Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.795Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.801Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:53.923Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.516Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.519Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.524Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.543Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.644Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.665Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.698Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.736Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.758Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.793Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.830Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.865Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.889Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.924Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:55.957Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.026Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.027Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.080Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.082Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.099Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.126Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.147Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.180Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.224Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.244Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.278Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.310Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.342Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.361Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.390Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.422Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.459Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.461Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.495Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.497Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.515Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.552Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.572Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.609Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.658Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.676Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.711Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.750Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.797Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.818Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.858Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.888Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.942Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.944Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.989Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:56.992Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.012Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.050Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.072Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.105Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.141Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.164Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.206Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.242Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.279Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.300Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.332Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.365Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.411Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.413Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.461Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.463Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.487Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.521Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.537Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.568Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.604Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.627Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.652Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.678Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.708Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.723Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.753Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.789Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.836Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.839Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.888Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.890Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.909Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.944Z info: Notification marked as completed {"id":21} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.959Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:57.994Z info: Notification marked for retry or failed +{"id":22,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.026Z info: Notification marked as completed {"id":22} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.060Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.092Z info: Notification marked for retry or failed +{"id":23,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.128Z info: Notification marked for retry or failed +{"id":23,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.160Z info: Notification marked as completed {"id":23} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.176Z info: Scheduled notification created {"id":24,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.225Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.257Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.307Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.310Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.359Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.363Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.385Z info: Scheduled notification created {"id":25,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.447Z info: Notification marked as completed {"id":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.464Z info: Scheduled notification created {"id":26,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.501Z info: Notification marked for retry or failed +{"id":26,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.540Z info: Notification marked as completed {"id":26} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.561Z info: Scheduled notification created {"id":27,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.592Z info: Notification marked for retry or failed +{"id":27,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.618Z info: Notification marked for retry or failed +{"id":27,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.649Z info: Notification marked as completed {"id":27} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.663Z info: Scheduled notification created {"id":28,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.688Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.712Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.756Z warn: Notification moved to dead letter queue +{"id":28,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.758Z info: Notification marked for retry or failed +{"id":28,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.801Z warn: Notification moved to dead letter queue +{"id":28,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.804Z info: Notification marked for retry or failed +{"id":28,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.823Z info: Scheduled notification created {"id":29,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.861Z info: Notification marked as completed {"id":29} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.886Z info: Scheduled notification created {"id":30,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.914Z info: Notification marked for retry or failed +{"id":30,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.945Z info: Notification marked as completed {"id":30} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.962Z info: Scheduled notification created {"id":31,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:58.994Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.029Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.063Z info: Notification marked as completed {"id":31} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.084Z info: Scheduled notification created {"id":32,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.116Z info: Notification marked for retry or failed +{"id":32,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.178Z info: Notification marked for retry or failed +{"id":32,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.234Z warn: Notification moved to dead letter queue +{"id":32,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.241Z info: Notification marked for retry or failed +{"id":32,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.294Z warn: Notification moved to dead letter queue +{"id":32,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.296Z info: Notification marked for retry or failed +{"id":32,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.315Z info: Scheduled notification created {"id":33,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.360Z info: Notification marked as completed {"id":33} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.383Z info: Scheduled notification created {"id":34,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.438Z info: Notification marked for retry or failed +{"id":34,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.480Z info: Notification marked as completed {"id":34} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.513Z info: Scheduled notification created {"id":35,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.555Z info: Notification marked for retry or failed +{"id":35,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.597Z info: Notification marked for retry or failed +{"id":35,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.643Z info: Notification marked as completed {"id":35} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.669Z info: Scheduled notification created {"id":36,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.716Z info: Notification marked for retry or failed +{"id":36,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.772Z info: Notification marked for retry or failed +{"id":36,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.864Z warn: Notification moved to dead letter queue +{"id":36,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.866Z info: Notification marked for retry or failed +{"id":36,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.926Z warn: Notification moved to dead letter queue +{"id":36,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.929Z info: Notification marked for retry or failed +{"id":36,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:21:59.949Z info: Scheduled notification created {"id":37,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.000Z info: Notification marked as completed {"id":37} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.045Z info: Scheduled notification created {"id":38,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.096Z info: Notification marked for retry or failed +{"id":38,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.141Z info: Notification marked as completed {"id":38} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.164Z info: Scheduled notification created {"id":39,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.215Z info: Notification marked for retry or failed +{"id":39,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.277Z info: Notification marked for retry or failed +{"id":39,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.324Z info: Notification marked as completed {"id":39} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.345Z info: Scheduled notification created {"id":40,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.408Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.444Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.508Z warn: Notification moved to dead letter queue +{"id":40,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.511Z info: Notification marked for retry or failed +{"id":40,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.576Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.580Z error: Database get error {"sql":"SELECT * FROM scheduled_notifications WHERE +id = ?","params":[40],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.583Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.720Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.749Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.760Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:00.809Z error: Database get error {"sql":"SELECT * FROM scheduled_notifications WHERE +id = ?","params":[40],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.711Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.714Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.717Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.741Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.784Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.809Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.831Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.865Z info: Fetched and locked pending notifications +{"count":1,"processorId":"test-processor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:01.881Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.024Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.058Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.061Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.065Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.096Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.139Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.168Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.191Z info: Notification cancelled {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.210Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.343Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.374Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.380Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.382Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.410Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.442Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.465Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.618Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.672Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.677Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.679Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.711Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.730Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.848Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.911Z info: Notification marked as completed {"id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:02.925Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.085Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.124Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.126Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.128Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.145Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.201Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.266Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.319Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":3,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.363Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":4,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.408Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":5,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.444Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":6,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.478Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":7,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.791Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":8,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.850Z warn: Notification moved to dead letter queue +{"id":9,"notificationType":"discord","failureReason":"Attempt 9 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.857Z info: Notification marked for retry or failed +{"id":9,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.891Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:03.899Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.052Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.093Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.098Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.112Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.132Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.183Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.208Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.244Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.268Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.309Z info: Notification marked as completed {"id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.331Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.362Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.381Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.419Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.441Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.491Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.527Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.548Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.579Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.628Z info: Notification marked as completed {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.652Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.697Z info: Notification marked for retry or failed +{"id":17,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.734Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.810Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.849Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:04.959Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.010Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.042Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.096Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.161Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.213Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.243Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.301Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.377Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.566Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.579Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.685Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.697Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:05.711Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +PASS src/__tests__/database-recovery.test.ts (9.829 s) + ● Console + + console.log + 2026-10-16T12:22:08.769Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.764Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.771Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.783Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.937Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('after')","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.940Z warn: Database connection failure detected; attempting recovery +{"path":"./data/test-db-recovery.db","attempt":1,"maxReconnectAttempts":3,"delayMs":10,"error":"SQLITE_MISUSE: +Database is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.979Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:09.983Z warn: Database connection recovered {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.067Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.079Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.864Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.869Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.877Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.884Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.891Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"maxReconnectAttempts":2,"delayMs":1,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.914Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.919Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.929Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.931Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"maxReconnectAttempts":2,"delayMs":2,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.941Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.945Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.948Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:10.979Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.294Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.300Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.316Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.478Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('dup')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.509Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.523Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.048Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.080Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.120Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.282Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('kept')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.409Z error: Transaction rolled back +{"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.533Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:15.612Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:17.731Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:17.750Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:17.766Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:17.778Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/tests/payload-integrity.test.ts + ● Console + + console.log + 2026-10-16T12:22:19.569Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.442Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.449Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.469Z info: Database initialized successfully {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.486Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.548Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.602Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.631Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.681Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.702Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.777Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.809Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.815Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/api/execution-metrics-api.test.ts (25.612 s) + ● Console + + console.log + 2026-10-16T12:22:11.931Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.350Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.390Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.409Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.710Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.852Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:13.997Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:14.190Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:17.629Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:17.668Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:18.901Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:18.920Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:18.925Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.016Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.096Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.162Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.315Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.445Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.498Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:19.557Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:20.349Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:20.350Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:21.408Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:21.418Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:21.494Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:21.528Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.624Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.629Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.633Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.700Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:22.730Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:24.661Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:24.664Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:24.669Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:24.718Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:24.729Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.642Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.644Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.647Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.673Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.696Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.735Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:25.801Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/services/discord-notification.test.ts + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   86 | await Promise.resolve(); +  87 | await Promise.resolve(); + > 88 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  89 | +  90 | queue.stop(); +  91 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:88:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   106 | await Promise.resolve(); +  107 | + > 108 | expect(queue.size()).toBe(0); +  | ^ +  109 | queue.stop(); +  110 | }); +  111 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:108:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4082.3624137134775, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:22:32.516Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   131 | await Promise.resolve(); +  132 | + > 133 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  134 | 'Retry succeeded', +  135 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  136 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:133:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   156 | await Promise.resolve(); +  157 | await Promise.resolve(); + > 158 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  159 | +  160 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  161 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:158:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   189 | await Promise.resolve(); +  190 | + > 191 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  192 | 'Retry failed, scheduling next attempt', +  193 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  194 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:191:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   218 | jest.advanceTimersByTime(100); +  219 | await flush(); + > 220 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  221 | +  222 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  223 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:220:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   255 | await Promise.resolve(); +  256 | + > 257 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  258 | 'Notification permanently failed after max retries', +  259 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  260 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:257:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:299:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   326 | } +  327 | + > 328 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  329 | queue.stop(); +  330 | }); +  331 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:328:30) + +FAIL src/database/event-notification-indexes.test.ts + ● Console + + console.log + 2026-10-16T12:22:30.134Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-26188-1792153350107.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:32.083Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:32.093Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:32.123Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-26188-1792153350107.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:32.154Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, +created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILE +D","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:32.210Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/api/events-server.test.ts (10.233 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › isDuplicate › returns true for a fingerprint that has been marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:41:9) + + ● NotificationDeduplicator › isDuplicate › returns false for a different fingerprint after another is marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:47:9) + + ● NotificationDeduplicator › markSent › increments size after marking a new fingerprint + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:55:9) + + ● NotificationDeduplicator › markSent › does not increment size when marking the same fingerprint twice + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:61:9) + + ● NotificationDeduplicator › markSent › evicts the oldest entry when the cache is full + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:68:9) + + ● NotificationDeduplicator › markSent › logs a warning when evicting due to full cache + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:83:9) + + ● NotificationDeduplicator › markSent › does not evict when cache has room + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:95:9) + + ● NotificationDeduplicator › time window and metrics › allows the same fingerprint after the deduplication window +expires + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:109:9) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:120:9) + + ● NotificationDeduplicator › time window and metrics › counts evicted and expired entries in metrics + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:140:9) + + ● NotificationDeduplicator › size › returns the correct count after multiple unique fingerprints + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:163:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › prevents a second notification for the +same event on re-poll + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:175:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › allows notifications for unique events +with the same contract + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:182:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › treats same event id on different +contracts as distinct + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:190:9) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "6ca84ad0"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "697e7796-0ec1-4523-87a8-18639feee515", +"eventId": "event-1", "requestId": "697e7796-0ec1-4523-87a8-18639feee515"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   109 | await Promise.resolve(); +  110 | await Promise.resolve(); + > 111 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  112 | +  113 | queue.stop(); +  114 | }); + + at Object. (src/services/notification-retry-queue.test.ts:111:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   128 | await Promise.resolve(); +  129 | + > 130 | expect(queue.size()).toBe(0); +  | ^ +  131 | queue.stop(); +  132 | }); +  133 | + + at Object. (src/services/notification-retry-queue.test.ts:130:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4950.209477735045, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:22:43.232Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   146 | await Promise.resolve(); +  147 | + > 148 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  149 | 'Retry succeeded', +  150 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  151 | ); + + at Object. (src/services/notification-retry-queue.test.ts:148:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   170 | await Promise.resolve(); +  171 | await Promise.resolve(); + > 172 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  173 | +  174 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  175 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:172:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   198 | await Promise.resolve(); +  199 | + > 200 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  201 | 'Retry failed, scheduling next attempt', +  202 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  203 | ); + + at Object. (src/services/notification-retry-queue.test.ts:200:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   225 | jest.advanceTimersByTime(100); +  226 | await flush(); + > 227 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  228 | +  229 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  230 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:227:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   257 | await Promise.resolve(); +  258 | + > 259 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  260 | 'Notification permanently failed after max retries', +  261 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  262 | ); + + at Object. (src/services/notification-retry-queue.test.ts:259:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue.test.ts:299:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   349 | +  350 | // The notification fn was called exactly 3 times + > 351 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  352 | +  353 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  354 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:351:30) + +FAIL src/tests/template-system.test.ts + ● Console + + console.log + 2026-10-16T12:22:37.034Z info: Connected to SQLite database {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:39.232Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:39.248Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:39.267Z info: Database initialized successfully {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:39.294Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX +IF NOT EXISTS idx_templates_unique_key \r\n ON +notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:39.323Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/tests/api-versioning.test.ts + ● Console + + console.log + 2026-10-16T12:22:41.732Z info: Handling GET /api/events +{"requestId":"84057549","correlationId":"9308d053-dc34-4e5b-b24b-a73d73836218","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.859Z info: GET /api/events complete +{"requestId":"84057549","correlationId":"9308d053-dc34-4e5b-b24b-a73d73836218","returned":0,"durationMs":128} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.865Z info: Handling GET /api/events +{"requestId":"2ff1284f","correlationId":"03ea9aa2-56ab-445f-a2ba-d24ad3359bd9","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.867Z info: GET /api/events complete +{"requestId":"2ff1284f","correlationId":"03ea9aa2-56ab-445f-a2ba-d24ad3359bd9","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.874Z info: Handling GET /api/events +{"requestId":"57f0e88e","correlationId":"3e2ce057-af48-4432-a5df-f0278839142c","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.876Z info: GET /api/events complete +{"requestId":"57f0e88e","correlationId":"3e2ce057-af48-4432-a5df-f0278839142c","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.881Z info: Handling GET /api/events +{"requestId":"abad97f5","correlationId":"ea41ad8c-7c3b-4e66-bb8b-96b0f9f3c7f8","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.882Z info: GET /api/events complete +{"requestId":"abad97f5","correlationId":"ea41ad8c-7c3b-4e66-bb8b-96b0f9f3c7f8","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.889Z info: Handling GET /api/events +{"requestId":"05dc138a","correlationId":"ce815d7d-5cb6-4e5b-8893-80ec7b01ac10","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.891Z info: GET /api/events complete +{"requestId":"05dc138a","correlationId":"ce815d7d-5cb6-4e5b-8893-80ec7b01ac10","returned":0,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.901Z info: Handling GET /api/events +{"requestId":"e1295f53","correlationId":"2059e0ff-e418-4b33-8a0a-3360b908abf7","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.903Z info: GET /api/events complete +{"requestId":"e1295f53","correlationId":"2059e0ff-e418-4b33-8a0a-3360b908abf7","returned":0,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.905Z info: Handling GET /api/events +{"requestId":"cacf39d5","correlationId":"f0c71501-8ecf-475f-9148-0d2c2af4d915","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.906Z info: GET /api/events complete +{"requestId":"cacf39d5","correlationId":"f0c71501-8ecf-475f-9148-0d2c2af4d915","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.911Z info: Handling GET /api/events +{"requestId":"d487697d","correlationId":"e42f043e-7c4a-4cb9-9802-ff230f1c52d9","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.912Z info: GET /api/events complete +{"requestId":"d487697d","correlationId":"e42f043e-7c4a-4cb9-9802-ff230f1c52d9","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.916Z info: Handling GET /api/events +{"requestId":"f2157e7f","correlationId":"9fb5a5ed-ab31-4837-9c49-2bfd1e4905b2","limit":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.918Z info: GET /api/events complete +{"requestId":"f2157e7f","correlationId":"9fb5a5ed-ab31-4837-9c49-2bfd1e4905b2","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.924Z warn: Unhandled request {"requestId":"2f043d55","correlationId":"daa93ae6-3e0b +-463a-8c88-00665a70c3ce","method":"GET","url":"/api/v1/unknown-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.926Z info: Request completed +{"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":2,"requestId":"2f043d55"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.970Z warn: Unhandled request {"requestId":"06ac4b4b","correlationId":"fd5bab50-d136 +-4b87-92a1-cf656d1f5a01","method":"GET","url":"/api/v1/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:41.973Z info: Request completed +{"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":5,"requestId":"06ac4b4b"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/services/notification-api-webhook.test.ts + ● Console + + console.log + 2026-10-16T12:22:42.371Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.511Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.514Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.518Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.522Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.561Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.564Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.567Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:42.571Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/api/archive-api.test.ts + ● Console + + console.log + 2026-10-16T12:22:43.978Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.011Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an +integer, received 'abc'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.017Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.021Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= +100"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.029Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.033Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= +0"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.038Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.040Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be +one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.044Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.052Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be +a valid date, received 'not-a-date'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.057Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.060Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.067Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:44.074Z info: Handling POST /api/archive/run {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/api/template-routes.test.ts (6.703 s) + ● Console + + console.log + 2026-10-16T12:22:45.142Z error: Failed to list templates {"error":{"message":"channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:45.160Z info: Listed templates via API +{"requestId":"req-1","count":0,"channelType":"EMAIL","activeOnly":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:45.188Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:45.193Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:85:33) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:107:33) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:117:33) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:137:33) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:153:33) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:201:33) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:215:33) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:226:33) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:237:33) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:247:33) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:261:33) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/tests/template-api-integration.test.ts (5.865 s) + ● Console + + console.log + 2026-10-16T12:22:49.985Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.247Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.259Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.378Z info: Database initialized successfully {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.403Z error: Database run error {"sql":"duplicate inserts with the same key are +silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.419Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue +(\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n +notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n +target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details +TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count +INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES +scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.460Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS +update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n +UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.462Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.465Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE +notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.467Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.470Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.472Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.473Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.477Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.592Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.609Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.619Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.633Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.654Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.662Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.666Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.670Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.675Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.677Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.680Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.683Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.685Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.689Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.694Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.698Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.700Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.702Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.713Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.718Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.721Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.725Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.729Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.732Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.735Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.743Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.746Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.750Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.759Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.762Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.765Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.768Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.775Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:22:52.818Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/webhook-delivery-service.test.ts (5.619 s) + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:79:22) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:89:22) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:98:7) + + ● WebhookDeliveryService › successful delivery (2xx) › passes the payload and options to sendWebhook + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:109:7) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:138:7) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:167:7) + + ● WebhookDeliveryService › request timeout (AbortError) › returns success: false when the request times out + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:183:22) + + ● WebhookDeliveryService › request timeout (AbortError) › logs a warn for timeouts so the caller knows to retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:193:7) + + ● WebhookDeliveryService › request timeout (AbortError) › includes the configured timeout in the errorReason + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:205:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › returns success: false on a generic network +error + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:217:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › logs an error for non-timeout network failures + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:226:7) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › handles non-Error thrown values gracefully + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:237:22) + + ● WebhookDeliveryService › default options › uses the instance-level timeout when none provided per-call + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:251:7) + + ● WebhookDeliveryService › default options › merges instance headers with per-call headers + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:264:7) + + ● WebhookDeliveryService › failure classification › labels a request timeout as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:282:22) + + ● WebhookDeliveryService › failure classification › labels a TimeoutError as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:292:22) + + ● WebhookDeliveryService › failure classification › labels a generic network error as "network" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:300:22) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:309:22) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:317:22) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:325:22) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792066978432} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792153379781, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792066978432} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792153379781, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "25a9b3a9"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Console + + console.log + 2026-10-16T12:23:00.884Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:00.952Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:00.967Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:00.978Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:00.985Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event from ingestion to notification +generation + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should handle multiple events in sequence + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing id + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with invalid ledger + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching wildcard filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching specific filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should reject event not matching filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should extract event name from topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should prevent duplicate notifications + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should allow same event from different contracts + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should handle notification failure gracefully + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should continue processing after individual event failure + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should process events quickly (< 100ms +per event) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should handle registry size limits + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should clean up resources properly + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should execute in under 2 seconds + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should make zero external network calls + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should run deterministically (same input = same +output) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +ts-jest[ts-compiler] (WARN) src/services/archive.test.ts:237:7 - error +TS2741: Property 'expiresAt' is missing in type '{ originalId: number; payload: string; notificationType: string; +targetRecipient: string; executeAt: string; createdAt: string; processingCompletedAt: string; status: string; +retryCount: number; lastError: null; eventId: null; contractAddress: null; metadata: null; }' but required in type '{ +originalId: number; payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: +string | null; createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +237 { +   ~ +238 originalId: 1, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +250 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +251 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +src/services/archive.test.ts:252:7 - error TS2741: Property 'expiresAt' +is missing in type '{ originalId: number; payload: string; notificationType: string; targetRecipient: string; +executeAt: string; createdAt: string; processingCompletedAt: null; status: string; retryCount: number; lastError: +string; eventId: string; contractAddress: string; metadata: null; }' but required in type '{ originalId: number; +payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: string | null; +createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +252 { +   ~ +253 originalId: 2, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +265 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +266 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +FAIL src/services/archive.test.ts + ● Console + + console.log + 2026-10-16T12:23:05.108Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T1 +2:23:05.107Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.152Z info: ArchiveService: cycle complete +{"archived":2,"purged":0,"durationMs":45,"jobId":"archive-cycle-1792153385095"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.157Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.181Z info: ArchiveService: archived processed notification immediately {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.186Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792153385185"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.189Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.194Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792153385193"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.197Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.201Z info: ArchiveService: purged expired archive records +{"count":2,"cutoff":"2026-07-18T12:23:05.201Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.204Z info: ArchiveService: cycle complete +{"archived":0,"purged":2,"durationMs":3,"jobId":"archive-cycle-1792153385200"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.207Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.212Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792153385211"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.215Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.220Z info: ArchiveService started +{"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.220Z info: Waiting for active archival jobs to complete {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.220Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.257Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":37,"jobId":"archive-cycle-1792153385220"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.259Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.265Z info: Handling GET /api/archive {"requestId":"req-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.275Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.280Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:05.286Z info: Handling POST /api/archive/run {"requestId":"req-6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/__tests__/notification-flow-e2e.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\__tests__\notification-flow-e2e.test.ts:302 + it(, async) { } + ^ + + SyntaxError: Unexpected token ',' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/webhook-verifier.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\services\webhook-verifier.test.ts:458 + expect(outcome) { }, : .authenticated + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +ts-jest[ts-compiler] (WARN) src/tests/events-server.test.ts:9:4 - error +TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value. + +9 ): Promise<{ status: number; headers: http.IncomingHttpHeaders }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:10:14 - error TS2552: Cannot find name +'headers'. Did you mean 'Headers'? + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~~ + + node_modules/@types/node/web-globals/fetch.d.ts:47:9 + 47 var Headers: typeof globalThis extends { onmessage: any; Headers: infer T } ? T : typeof +undici.Headers; +    ~~~~~~~ + 'Headers' is declared here. +src/tests/events-server.test.ts:10:24 - error TS2693: 'Record' only +refers to a type, but is being used as a value here. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:10:48 - error TS2304: Cannot find name +'method'. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:10:57 - error TS2693: 'string' only +refers to a type, but is being used as a value here. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:11:4 - error TS2365: Operator '<' cannot +be applied to types 'PromiseConstructor' and '{ status: any; headers: any; body: any; }'. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:11:4 - error TS2365: Operator '>' cannot +be applied to types 'boolean' and '{ return: Promise; }'. + + 11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 return new Promise((resolve, reject) => { +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 26 }); +  ~~~~~ + 27 } +  ~ +src/tests/events-server.test.ts:11:22 - error TS2693: 'number' only +refers to a type, but is being used as a value here. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~ +src/tests/events-server.test.ts:11:44 - error TS2339: Property +'IncomingHttpHeaders' does not exist on type 'typeof import("http")'. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:11:71 - error TS2693: 'any' only refers +to a type, but is being used as a value here. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~ +src/tests/events-server.test.ts:150:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +150 const { status, body } = await makeRequest('/api/events'); +   ~~~~ +src/tests/events-server.test.ts:157:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +157 const { status, body } = await makeRequest('/api/events?limit=50'); +   ~~~~ +src/tests/events-server.test.ts:163:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +163 const { status, body } = await makeRequest('/api/events?limit=200'); +   ~~~~ +src/tests/events-server.test.ts:169:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +169 const { status, body } = await makeRequest('/api/events?limit=-10'); +   ~~~~ +src/tests/events-server.test.ts:175:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +175 const { status, body } = await makeRequest('/api/events?limit=invalid'); +   ~~~~ +src/tests/events-server.test.ts:10:22 - error TS1109: Expression expected. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:10:55 - error TS1109: Expression expected. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:10:66 - error TS2809: Declaration or +statement expected. This '=' follows a block of statements, so if you intended to write a destructuring assignment, +you might need to wrap the whole assignment in parentheses. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:11:1 - error TS1128: Declaration or +statement expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +  ~ +src/tests/events-server.test.ts:11:2 - error TS1128: Declaration or +statement expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:11:28 - error TS1005: ',' expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:11:63 - error TS1005: ',' expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:12:10 - error TS1005: ':' expected. + +12 return new Promise((resolve, reject) => { +   ~~~ +src/tests/events-server.test.ts:23:5 - error TS1128: Declaration or +statement expected. + +23 ); +   ~ +src/tests/events-server.test.ts:26:5 - error TS1005: ',' expected. + +26 }); +   ~ +src/tests/events-server.test.ts:180:1 - error TS1005: '}' expected. + +180 +   + + src/tests/events-server.test.ts:77:46 + 77 describe('GET /api/events pagination', () => { +    ~ + The parser expected to find a '}' to match the '{' token here. +FAIL src/tests/events-server.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\tests\events-server.test.ts:11 + headers ? : Record; + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/tests/notification-scheduler-refactored.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\tests\notification-scheduler-refactored.test.ts:271 + const pastLock = notification_fixture_builder_1.NotificationFixtureBuilder.dates.past(1000); + ^ + + SyntaxError: Identifier 'pastLock' has already been declared + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +PASS src/scripts/replay-event.test.ts + ● Console + + console.log + 2026-10-16T12:23:10.606Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.629Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.632Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.644Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.650Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.653Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.664Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.666Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.667Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.670Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.674Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.685Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.686Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.688Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.702Z info: Event processed and recorded {"eventId":"duplicate-test-event","contract +Address":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a0 +26ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.725Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.730Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.744Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.745Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.747Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.750Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAd +dress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de +1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.752Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.755Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.768Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.770Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.772Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.775Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.778Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.790Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.792Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.794Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.797Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.800Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.812Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.813Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.815Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:10.818Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts + ● Console + + console.log + 2026-10-16T12:23:13.446Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.467Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.472Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.475Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.799Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.801Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.802Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:13.804Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:14.595Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:14.598Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:14.600Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:14.601Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:15.006Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:15.008Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:15.011Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:15.012Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/payload-validation.integration.test.ts +PASS src/config/schema-validator.test.ts +PASS src/utils/pagination.test.ts +FAIL src/__tests__/query-performance.test.ts + ● Test suite failed to run + + Cannot find module 'vitest' from 'src/__tests__/query-performance.test.ts' + + > 1 | import { describe, it, expect } +from "vitest"; // if the repo uses jest, DELETE this line +  | ^ +  2 | import { readFileSync, writeFileSync, mkdirSync } from +"node:fs"; +  3 | import { dirname, resolve } from "node:path"; +  4 | import { performance } from "node:perf_hooks"; + + at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/index.js:1031:11) + at Object. (src/__tests__/query-performance.test.ts:1:1) + +PASS src/config-logging.test.ts +PASS src/utils/circuit-breaker.test.ts +PASS src/services/circuit-breaker.test.ts + ● Console + + console.log + 2026-10-16T12:23:23.072Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.102Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.107Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.110Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.113Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.117Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.120Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.122Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.125Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153404125} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.127Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.130Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.132Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.134Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153404133} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.135Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.137Z warn: Circuit breaker is OPEN, blocking request +{"state":"open","nextAttemptTime":1792153404133} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.141Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.143Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.145Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153404145} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:23.147Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:24.262Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:24.265Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:24.267Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:24.269Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153405269} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:24.270Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.386Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.387Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.391Z info: Circuit breaker closed after successful recovery +{"successCount":0,"successThreshold":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.392Z info: Circuit breaker recorded success +{"state":"closed","successCount":0,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.395Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.397Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.398Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153406398} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.400Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.534Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.581Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.597Z error: Circuit breaker opened due to repeated failures +{"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153407596} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.598Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.604Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.606Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.608Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153407608} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.611Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.613Z info: Circuit breaker manually reset {"previousState":"closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.631Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.636Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.645Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.647Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.649Z error: Circuit breaker opened due to repeated failures +{"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792153407648} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.652Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":5,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.655Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/database/query-performance.test.ts (11.784 s) + ● Console + + console.log + 2026-10-16T12:23:25.147Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792153405139.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.992Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:25.995Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:26.020Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792153405139.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.088Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.097Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status, priority)\n VALUES (?, 'discord', ?, +datetime('now', ?), 'PENDING', ?)","params":["{\"i\":119}","user-119","-119 +seconds",10],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.100Z warn: Database connection failure detected; attempting recovery {"path":"C:\\U +sers\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792153405139.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100, +"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   44 | }); +  45 | + > 46 | it('uses an indexed plan for pending claim queries', +async () => { +  | ^ +  47 | // Seed enough rows that a full scan would be meaningful +  48 | for (let i = 0; i < +200; i++) { +  49 | await db.run( + + at src/database/query-performance.test.ts:46:3 + at Object. (src/database/query-performance.test.ts:20:1) + +PASS src/api/rate-limit-scenarios.test.ts (6.505 s) +PASS src/services/worker-manager.test.ts + ● Console + + console.log + 2026-10-16T12:23:31.647Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.680Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.684Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.686Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.705Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.709Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.713Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.717Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.719Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.725Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:31.730Z info: Initiating graceful shutdown {"activeJobs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:32.751Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:32.754Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:32.761Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:32.768Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:32.777Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.783Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.786Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.794Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.798Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.802Z warn: Graceful shutdown already initiated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.804Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:33.817Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:33.841Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-metrics-store.test.ts +PASS src/utils/redact.test.ts +PASS src/services/event-deduplication-service.test.ts +PASS src/tests/notification-scheduler.test.ts + ● Console + + console.log + 2026-10-16T12:23:36.982Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.744Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.748Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.776Z info: Database initialized successfully +{"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.810Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.843Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.858Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.864Z info: Processing scheduled notification +{"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:37.868Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":1073} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:37.909Z info: Notification marked as completed {"requestId":"test-request-id","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:37.929Z info: Notification delivered successfully +{"requestId":"test-request-id","id":1,"type":"webhook","durationMs":31} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:37.933Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-success-test","count":1,"durationMs":107} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:37.941Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.010Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.031Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.033Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.035Z info: Processing scheduled notification +{"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.037Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":1041} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.043Z error: Failed to process notification +{"requestId":"test-request-id","id":2,"error":{"message":"secret +token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.078Z error: Job monitor: job failed +{"jobId":"notification-2","jobName":"scheduled-notification","error":"secret token=should-not-persist","durationMs":43} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.103Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:23:43.088Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.125Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":108} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.129Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.188Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.242Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.262Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.295Z info: Expired notification skipped before delivery +{"requestId":"test-request-id","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.362Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.386Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.388Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.391Z info: Processing scheduled notification +{"requestId":"test-request-id","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.393Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":5,"executeAt":{},"now":{},"missedByMs":60047} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.396Z error: Failed to process notification +{"requestId":"test-request-id","id":5,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary +outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.399Z error: Job monitor: job failed +{"jobId":"notification-5","jobName":"scheduled-notification","error":"temporary outage","durationMs":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.421Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:23:38.502Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.444Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":78} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.559Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.567Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-expiration-test","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.594Z info: Expired retry skipped before delivery +{"requestId":"test-request-id","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.598Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.646Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.661Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.678Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.682Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.684Z info: Processing scheduled notification +{"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.686Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1052} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.703Z info: Notification marked as completed {"requestId":"test-request-id","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.721Z info: Notification delivered successfully +{"requestId":"test-request-id","id":6,"type":"discord","durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.726Z info: Processing scheduled notification +{"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.727Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":1079} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.746Z info: Notification marked as completed {"requestId":"test-request-id","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.763Z info: Notification delivered successfully +{"requestId":"test-request-id","id":7,"type":"discord","durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.767Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":104} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.772Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.834Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.846Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.862Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.902Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.918Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.920Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.922Z info: Processing scheduled notification +{"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.923Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60035} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.926Z error: Failed to process notification +{"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: +temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.928Z error: Job monitor: job failed +{"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.942Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:23:40.930Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T12:23:38.956Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":52} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.994Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.996Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:38.998Z info: Retrying notification +{"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.012Z info: Notification marked as completed {"requestId":"test-request-id","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.028Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.032Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.077Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.094Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.143Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.161Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.193Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.249Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.263Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.310Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.325Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.369Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.398Z warn: Notification moved to dead letter queue +{"id":15,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.400Z info: Notification marked for retry or failed +{"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.443Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.460Z info: Notification cancelled {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.504Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.524Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.569Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.586Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.620Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.648Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.727Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.741Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.769Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.784Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.817Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.833Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.843Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:39.848Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.780Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.781Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.783Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.804Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.825Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.867Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.887Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.931Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.966Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:40.968Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:41.015Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:41.045Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-stale"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:41.085Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:41.143Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:41.159Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:41.165Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/rate-limiter.test.ts (7.818 s) +PASS src/services/subscription-service.test.ts +PASS src/services/dead-letter-queue.test.ts + ● Console + + console.log + 2026-10-16T12:23:42.571Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.297Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.300Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.311Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.330Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.362Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.367Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T12:23:48.332Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.388Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:43.412Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.250Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.252Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.258Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.280Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.320Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"email","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.324Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T12:23:49.283Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.347Z info: Dead-letter notification requeued +{"requestId":"req-2","id":1,"notificationId":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:44.354Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/events-server.history.test.ts +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/services/search-suggestion.test.ts + ● Console + + console.log + 2026-10-16T12:23:45.658Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:45.680Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:45.683Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:45.700Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:45.787Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-stats-cache.test.ts +PASS src/utils/payload-size-validator.test.ts +PASS src/services/rpc-rate-limiter.test.ts + ● Console + + console.log + 2026-10-16T12:23:45.993Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.075Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.135Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.199Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.261Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.434Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.449Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.464Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.479Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.497Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.510Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.526Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.543Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.558Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.574Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.591Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.608Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.621Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.637Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.652Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.667Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.683Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.698Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.714Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.730Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.745Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.762Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.779Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.794Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.810Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.825Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.842Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.857Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.872Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.887Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.903Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.918Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.935Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.950Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.966Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.982Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:46.999Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.014Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.030Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.045Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.061Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.077Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.093Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.108Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.126Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.141Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.155Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.170Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.186Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.202Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.218Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.233Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.249Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.265Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.280Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.295Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.311Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.326Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.342Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.357Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.376Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.391Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.405Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:47.420Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-normalization.test.ts +PASS src/__tests__/api-contract.test.ts + ● Console + + console.log + 2026-10-16T12:23:48.247Z info: Handling GET /api/events +{"requestId":"10dc0713","correlationId":"7ecada85-2e5e-4394-a934-6b2c6312cc34","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.268Z info: GET /api/events complete +{"requestId":"10dc0713","correlationId":"7ecada85-2e5e-4394-a934-6b2c6312cc34","returned":0,"durationMs":22} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.292Z warn: Unhandled request {"requestId":"8cbf801c","correlationId":"9b5c0a3b-095b +-4ccc-8cf4-aaf52f462a63","method":"GET","url":"/api/definitely-not-a-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.296Z info: Request completed +{"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":4,"requestId":"8cbf801c"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.307Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.315Z info: Handling GET /api/analytics {"requestId":"e92a59cd","correlationId":"4bc +f3bc3-5089-4309-924a-7ad9a9b9067d","totalRecorded":0,"reset":false,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.321Z info: Handling GET /api/events +{"requestId":"03e8707c","correlationId":"94b610c9-863b-42bd-af1f-d9a0dc45c13e","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:48.323Z info: GET /api/events complete +{"requestId":"03e8707c","correlationId":"94b610c9-863b-42bd-af1f-d9a0dc45c13e","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/templates-api.test.ts +PASS src/store/event-registry.test.ts +PASS src/schema/compatibility-check.test.ts +PASS src/services/database-cleanup-job.test.ts + ● Console + + console.log + 2026-10-16T12:23:49.704Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.217Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.245Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.287Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load.test.ts + ● Console + + console.log + + === Load Test Results (100 events) === + + at Object. (src/__tests__/load.test.ts:61:15) + + console.log + Total processing time: 189ms + + at Object. (src/__tests__/load.test.ts:62:15) + + console.log + Throughput: 529.10 events/second + + at Object. (src/__tests__/load.test.ts:63:15) + + console.log + Average latency per event: 1.89ms + + at Object. (src/__tests__/load.test.ts:64:15) + + console.log + + === Load Test Results (500 events, maxConcurrency=20) === + + at Object. (src/__tests__/load.test.ts:108:15) + + console.log + Total processing time: 428ms + + at Object. (src/__tests__/load.test.ts:109:15) + + console.log + Throughput: 1168.22 events/second + + at Object. (src/__tests__/load.test.ts:110:15) + + console.log + Average latency per event: 0.86ms + + at Object. (src/__tests__/load.test.ts:111:15) + +PASS src/services/template-service.test.ts + ● Console + + console.log + 2026-10-16T12:23:50.966Z info: Template created successfully +{"templateId":1,"uniqueKey":"welcome-email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.001Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome +{{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.008Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" +","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.014Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.020Z error: Failed to create template {"error":{"message":"description: must be a +string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","description":12345}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.026Z error: Failed to create template {"error":{"message":"name: must not exceed +255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTe +mplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.032Z error: Failed to create template {"error":{"message":"variables: must be an +array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","variables":[1,2,3]}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.037Z error: Failed to create template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","defaultValues":"nope"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.045Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.049Z error: Failed to update template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.052Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.060Z error: Failed to update template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-repository.test.ts + ● Console + + console.log + 2026-10-16T12:23:50.532Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.558Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.563Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.569Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.576Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.594Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.600Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.613Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.614Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.616Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.627Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.631Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.650Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.652Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.653Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.661Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.678Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.715Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.718Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.719Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.727Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.731Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.745Z info: Notification template updated +{"templateId":"tmpl-001","actor":"admin@example.com"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.770Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.779Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.814Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.816Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.818Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.820Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.828Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.831Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.834Z info: Template audit record created +{"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.838Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.845Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.850Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.863Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.864Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.866Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.868Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.870Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.877Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.888Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.890Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.892Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.894Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.898Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.900Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.914Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.916Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.918Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.921Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.928Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.931Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.943Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.945Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.946Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.948Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.950Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.960Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.962Z info: [TemplateCache] Template invalidated +{"templateId":"tmpl-001","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.964Z info: Notification template updated +{"templateId":"tmpl-001","actor":"cache-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.966Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.968Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.979Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.980Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.982Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.984Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.986Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.988Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.994Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET +actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.996Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:50.999Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.012Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.013Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.015Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.017Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.019Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.021Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.031Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log +WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.034Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.036Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.049Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.050Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.051Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.055Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.060Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.072Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.077Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.079Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.084Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-cache.test.ts + ● Console + + console.log + 2026-10-16T12:23:51.268Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.297Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.299Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.303Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.311Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.313Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.318Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.320Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.324Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.327Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.330Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.332Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.335Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.337Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.342Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.344Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.347Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.349Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.350Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.353Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.356Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.360Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.363Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.366Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.369Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.377Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.381Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.385Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/migrations/005-notification-expiration.test.ts + ● Console + + console.log + 2026-10-16T12:23:51.567Z info: Migration 005 (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.649Z info: Migration 005-repeat (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.670Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:51.692Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/store/preference-store.test.ts +PASS src/examples/webhook-consumer-verification-example.test.ts +PASS src/services/notification-api.test.ts + ● Console + + console.log + 2026-10-16T12:23:52.240Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.295Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.298Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.313Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.318Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.335Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-search-service.test.ts + ● Console + + console.log + 2026-10-16T12:23:52.551Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.576Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.579Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.583Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:52.587Z info: Notification search complete +{"total":1,"returned":1,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/indexing-reconciliation-engine.test.ts +PASS src/api/delivery-metrics-api.test.ts + ● Console + + console.log + 2026-10-16T12:23:53.030Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"b33710cb" +,"correlationId":"6ab4e02c-65d4-492b-8105-faa69e5e88bb","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.069Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"a4840890" +,"correlationId":"c642bac8-16be-49a3-b178-e91bffdbee79","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.080Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"e5d8fc60" +,"correlationId":"726a9de1-855f-432a-b88a-70deef9e0a1d","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs" +:0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.081Z info: Delivery metrics window reset after read +{"requestId":"e5d8fc60","correlationId":"726a9de1-855f-432a-b88a-70deef9e0a1d"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.092Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"56e6bc41" +,"correlationId":"88208b02-42f1-4c14-9f0f-001edc45fdb1","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.103Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"af9937e7" +,"correlationId":"dfa9fa1a-c127-47f2-a204-529e5480b074","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.115Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"c03fbac5","correlationId":"ebb2e71e-4ecc-46a3-9eb9-a9098774d909","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.118Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"c03fbac5","correlationId":"ebb2e71e-4ecc-46a3-9eb9-a9098774d909","count":1,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.135Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"0d605c43","correlationId":"ffc95579-98e8-4428-a23f-ff5a588bba16","limit":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.138Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"0d605c43","correlationId":"ffc95579-98e8-4428-a23f-ff5a588bba16","count":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.148Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"e +fc4c4d9","correlationId":"001b79db-9f5c-429a-9508-436cd44a7381","limit":50,"since":"2024-01-01T00:00:00.000Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.150Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"efc4c4d9","correlationId":"001b79db-9f5c-429a-9508-436cd44a7381","count":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.175Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"74fb6869","correlationId":"29f1db5d-6c08-444c-8848-c20199770f19","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.177Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"74fb6869","correlationId":"29f1db5d-6c08-444c-8848-c20199770f19","count":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/api-version-registry.test.ts +PASS src/utils/app-version.test.ts +PASS src/services/scheduled-notification-repository-cache.test.ts + ● Console + + console.log + 2026-10-16T12:23:53.455Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.471Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.474Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.482Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.502Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.514Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.519Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.525Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.528Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.529Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.531Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.534Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.536Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.538Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.540Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.542Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.544Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + Performance comparison: + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:130:15) + + console.log + Uncached call: 1ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:131:15) + + console.log + Cached call: 0ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:132:15) + + console.log + Improvement: 100.0% + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:133:15) + + console.log + 2026-10-16T12:23:53.550Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.553Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.555Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.558Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.560Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.564Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.566Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.567Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.581Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-service.test.ts + ● Console + + console.log + 2026-10-16T12:23:53.612Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.631Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.633Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.663Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.667Z info: Notification template created {"templateId":"svc-template"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.713Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.720Z info: Template audit record created +{"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.727Z info: [TemplateCache] Template invalidated +{"templateId":"svc-template","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.728Z info: Notification template updated +{"templateId":"svc-template","actor":"service-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.731Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.734Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.737Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.750Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.752Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.753Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.758Z info: Notification template created {"templateId":"tmpl-a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.759Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.761Z info: Notification template created {"templateId":"tmpl-b"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.762Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.765Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.768Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.780Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.781Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.782Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.784Z info: Notification template created {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.786Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.791Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.793Z info: Notification template deleted {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.794Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.796Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.799Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.812Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.814Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.816Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.820Z info: Notification template created {"templateId":"render-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.822Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.827Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.829Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.842Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.844Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.846Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.850Z info: Notification template created {"templateId":"render-missing"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.851Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.863Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.868Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.879Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.880Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.882Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.885Z info: Notification template created {"templateId":"no-vars"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.886Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:53.888Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/notifications-history.test.ts +PASS src/services/backpressure-controller.test.ts + ● Console + + console.log + 2026-10-16T12:23:55.451Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.483Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.485Z info: Backpressure deactivated: queue recovered +{"queueSize":49,"threshold":50,"durationMs":3,"targetThroughputPerSec":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.489Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.496Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.499Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.503Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.512Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:55.515Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/events-server.health.test.ts +PASS src/api/content-type.test.ts +PASS src/utils/request-id.test.ts +PASS src/services/providers/provider-health.test.ts + ● Console + + console.log + 2026-10-16T12:23:57.336Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/cors-validator.test.ts +PASS src/utils/event-utils.test.ts +PASS src/services/webhook-replay-cache.test.ts +PASS src/config-schema.test.ts +PASS src/utils/logger.test.ts +PASS src/services/dead-letter-manager.test.ts +PASS src/services/event-store-consistency.test.ts + ● Console + + console.log + 2026-10-16T12:23:59.212Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:59.233Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:59.237Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:59.246Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:23:59.295Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/request-size-limiter.test.ts +PASS src/utils/logger-structured.test.ts +PASS src/utils/validation.test.ts +PASS src/services/retry-statistics.test.ts (7.295 s) +PASS src/services/subscription-event-emitter.test.ts +PASS src/middleware/body-limit.test.ts +PASS src/services/notification-analytics-aggregator.test.ts +PASS src/utils/payload-compression.test.ts + ● Console + + console.log + 2026-10-16T12:24:05.950Z info: Payload compressed +{"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:06.077Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:06.082Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-provenance.test.ts + ● Console + + console.log + 2026-10-16T12:24:06.403Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:06.431Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:06.434Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:06.442Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:06.469Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/shutdown-unit.test.ts +PASS src/services/retry-scheduler-webhook.test.ts +PASS src/services/notification-metrics-runner.test.ts +PASS src/services/provider-registry.test.ts + ● Console + + console.log + 2026-10-16T12:24:07.927Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock +Provider","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:07.957Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing +Mock","capabilities":[]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:07.963Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial +Mock","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-scheduler.test.ts + ● Console + + console.log + 2026-10-16T12:24:08.229Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.265Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.267Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.269Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.280Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.282Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.284Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.286Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.294Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.300Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.306Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.312Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.317Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load-test-runner.test.ts (6.923 s) +PASS src/services/notification-import-service.test.ts + ● Console + + console.log + 2026-10-16T12:24:08.566Z info: Bulk notification import complete +{"imported":2,"skipped":0,"total":2,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.602Z info: Bulk notification import complete +{"imported":1,"skipped":2,"total":3,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:08.607Z info: Bulk notification import complete +{"imported":1,"skipped":1,"total":2,"format":"csv"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/idempotency-key-service.test.ts +PASS src/providers/provider-capabilities.test.ts +PASS src/utils/cors-validator.test.ts +PASS src/__tests__/integration.test.ts + ● Console + + console.log + 2026-10-16T12:24:09.096Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":2,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/json-logger-formatter.test.ts +PASS src/utils/metadata-validator.test.ts +PASS src/utils/batch-validator.test.ts +PASS src/services/historical-backfill-service.test.ts +PASS src/services/event-quarantine.test.ts +PASS src/services/job-monitor.test.ts + ● Console + + console.log + 2026-10-16T12:24:09.865Z error: Job monitor: job failed +{"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/config-snapshot.test.ts +PASS src/services/delivery-receipt-repository.test.ts +PASS src/middleware/request-id.test.ts +PASS src/utils/log-level-resolver.test.ts +PASS src/services/event-subscriber-checkpoint.test.ts +PASS src/services/stellar-rpc-manager.test.ts +PASS src/migrations/004-database-data-integrity.test.ts + ● Console + + console.log + 2026-10-16T12:24:11.566Z info: Migration 004 (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.663Z info: Migration 004-repeat (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.712Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 +aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 +row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data +first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.729Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.742Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.744Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.746Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.751Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', +'2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:11.753Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-policy.test.ts +PASS src/utils/ledger-range-validator.test.ts +PASS src/services/event-processing-queue.test.ts +PASS src/services/dns-rebinding-guard.test.ts +PASS src/test-utils/notification-fixture-builder.test.ts +PASS src/services/notification-health-monitor.test.ts + ● Console + + console.log + 2026-10-16T12:24:14.573Z info: NotificationHealthMonitor started {"intervalMs":30000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:24:14.621Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/notification-version.test.ts +PASS src/middleware/security-headers.test.ts +ts-jest[ts-compiler] (WARN) src/api/__tests__/api-contract.test.ts:20:46 - +error TS2345: Argument of type '{ status: string; }' is not assignable to parameter of type 'never'. + +20 getHealth: jest.fn().mockResolvedValue({ status: 'healthy' }), +   ~~~~~~~~~~~~~~~~~~~~~ +src/api/__tests__/api-contract.test.ts:60:38 - error TS2345: Argument of +type '{ ok: number; }' is not assignable to parameter of type 'never'. + +60 get: jest.fn().mockResolvedValue({ ok: 1 }), +   ~~~~~~~~~ +PASS src/api/__tests__/api-contract.test.ts (6.713 s) +A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due +to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure +that .unref() was called on them. + +Summary of all failing tests +FAIL src/services/discord-notification-refactored.test.ts (87.284 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (50.827 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   168 | +  169 | describe('fan-out delivery', () => { + > 170 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  171 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  172 | +  173 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   177 | }); +  178 | + > 179 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  180 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  181 | +  182 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:179:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:169:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   193 | +  194 | describe('channel-specific payloads', () => { + > 195 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  196 | await deliverToAllChannels( +  197 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  198 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   209 | }); +  210 | + > 211 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  212 | await deliverToAllChannels( +  213 | createMockEvent({ id: 'event-system', type: +'system' }) +  214 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:211:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:194:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   224 | +  225 | describe('partial delivery failure', () => { + > 226 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  227 | +router.setHandler(channels[1].config.webhookUrl, () => +  228 | failedFetch(500, 'Internal Server Error') +  229 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   234 | }); +  235 | + > 236 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  237 | +router.setHandler(channels[1].config.webhookUrl, () => +  238 | failedFetch(429, 'Too Many Requests', +'rate limited') +  239 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:236:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   259 | }); +  260 | + > 261 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  262 | +router.setHandler(channels[2].config.webhookUrl, () => { +  263 | throw new Error('ECONNRESET'); +  264 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:261:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:225:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   288 | }; +  289 | + > 290 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  291 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  292 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  293 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:290:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   314 | }); +  315 | + > 316 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  317 | // 'audit' never recovers. +  318 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  319 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:316:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   339 | }); +  340 | + > 341 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  342 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  343 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  344 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:341:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:276:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:113:1) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (19.154 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/__tests__/scheduler-concurrency.test.ts (11.803 s) + ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   123 | }); +  124 | + > 125 | test('concurrency larger than the batch is clamped to the batch +size', async () => { +  | ^ +  126 | await seedDueNotifications(2); +  127 | await sleep(1300); +  128 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:125:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +FAIL src/__tests__/stress.test.ts (209.354 s) + ● Stress Test Suite: Notification Processing › 6. Discord Notification Service Under Load › should handle 500 +Discord notifications with deduplication + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: 0 + +   631 | console.log(` Duplicates Skipped: +${metrics.duplicatesSkipped}`); +  632 | + > 633 | +expect(successCount.value).toBe(eventCount); +  | ^ +  634 | expect(report.throughput).toBeGreaterThan(50); +  635 | }, 120000); +  636 | }); + + at Object. (src/__tests__/stress.test.ts:633:34) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   637 | +  638 | describe('7. Error Recovery Under Stress', () => { + > 639 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  640 | const eventCount = 1000; +  641 | const processedIds: string[] = []; +  642 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:639:5 + at src/__tests__/stress.test.ts:638:3 + at Object. (src/__tests__/stress.test.ts:37:1) + +FAIL src/api/queue-operational-metrics.test.ts (21.512 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + + ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — +Dedicated Endpoint › supports aliases /api/schedule/queue-metrics and /api/notifications/queue-metrics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let testDbPath: string; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | testDbIndex++; +  77 | testDbPath = path.join( +  78 | __dirname, + + at src/api/queue-operational-metrics.test.ts:75:13 + at Object. (src/api/queue-operational-metrics.test.ts:68:9) + +FAIL src/services/scheduled-notification-queue.test.ts (9.543 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (9.915 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/services/concurrent-event-processing.test.ts (21.241 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 3) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | let dbB: Database; +  129 | + > 130 | beforeEach(async () => { +  | ^ +  131 | jest.clearAllMocks(); +  132 | dbPath = path.join(os.tmpdir(), +`notify-concurrency-${process.pid}-${Date.now()}-${Math.random()}.db`); +  133 | dbA = await newDatabase(dbPath); + + at src/services/concurrent-event-processing.test.ts:130:3 + at Object. (src/services/concurrent-event-processing.test.ts:125:1) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + EBUSY: resource busy or locked, unlink +'C:\Users\NEWOWN~1\AppData\Local\Temp\notify-concurrency-13144-1792153303525-0.7452762566666198.db' + +   140 | await dbB.close(); +  141 | for (const suffix of ['', +'-journal', '-wal', '-shm']) { + > 142 | if (fs.existsSync(dbPath + suffix)) +fs.unlinkSync(dbPath + suffix); +  | ^ +  143 | } +  144 | }); +  145 | + + at Object. (src/services/concurrent-event-processing.test.ts:142:46) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + EBUSY: resource busy or locked, unlink +'C:\Users\NEWOWN~1\AppData\Local\Temp\notify-concurrency-13144-1792153308581-0.9283555795994962.db' + +   140 | await dbB.close(); +  141 | for (const suffix of ['', +'-journal', '-wal', '-shm']) { + > 142 | if (fs.existsSync(dbPath + suffix)) +fs.unlinkSync(dbPath + suffix); +  | ^ +  143 | } +  144 | }); +  145 | + + at Object. (src/services/concurrent-event-processing.test.ts:142:46) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/execution-metrics.test.ts (9.112 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/services/retry-deduplication.test.ts (16.158 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/tests/payload-integrity.test.ts + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/api/execution-metrics-api.test.ts (25.612 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/services/discord-notification.test.ts + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   86 | await Promise.resolve(); +  87 | await Promise.resolve(); + > 88 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  89 | +  90 | queue.stop(); +  91 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:88:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   106 | await Promise.resolve(); +  107 | + > 108 | expect(queue.size()).toBe(0); +  | ^ +  109 | queue.stop(); +  110 | }); +  111 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:108:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4082.3624137134775, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:22:32.516Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   131 | await Promise.resolve(); +  132 | + > 133 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  134 | 'Retry succeeded'[39m, +  135 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  136 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:133:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   156 | await Promise.resolve(); +  157 | await Promise.resolve(); + > 158 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  159 | +  160 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  161 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:158:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   189 | await Promise.resolve(); +  190 | + > 191 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  192 | 'Retry failed, scheduling next attempt', +  193 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  194 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:191:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   218 | jest.advanceTimersByTime(100); +  219 | await flush(); + > 220 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  221 | +  222 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  223 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:220:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   255 | await Promise.resolve(); +  256 | + > 257 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  258 | 'Notification permanently failed after max retries', +  259 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  260 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:257:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:299:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   326 | } +  327 | + > 328 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  329 | queue.stop(); +  330 | }); +  331 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:328:30) + +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/api/events-server.test.ts (10.233 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › isDuplicate › returns true for a fingerprint that has been marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:41:9) + + ● NotificationDeduplicator › isDuplicate › returns false for a different fingerprint after another is marked sent + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:47:9) + + ● NotificationDeduplicator › markSent › increments size after marking a new fingerprint + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:55:9) + + ● NotificationDeduplicator › markSent › does not increment size when marking the same fingerprint twice + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:61:9) + + ● NotificationDeduplicator › markSent › evicts the oldest entry when the cache is full + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:68:9) + + ● NotificationDeduplicator › markSent › logs a warning when evicting due to full cache + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:83:9) + + ● NotificationDeduplicator › markSent › does not evict when cache has room + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:95:9) + + ● NotificationDeduplicator › time window and metrics › allows the same fingerprint after the deduplication window +expires + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:109:9) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:120:9) + + ● NotificationDeduplicator › time window and metrics › counts evicted and expired entries in metrics + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:140:9) + + ● NotificationDeduplicator › size › returns the correct count after multiple unique fingerprints + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:163:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › prevents a second notification for the +same event on re-poll + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:175:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › allows notifications for unique events +with the same contract + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:182:9) + + ● NotificationDeduplicator › duplicate prevention across repeated events › treats same event id on different +contracts as distinct + + TypeError: logger_1.default.debug is not a function + +   135 | }); +  136 | + > 137 | logger.debug('Notification marked as sent', +{ +  | ^ +  138 | fingerprint, +  139 | cacheSize: this.seen.size, +  140 | acceptedTotal: this.acceptedRequests, + + at NotificationDeduplicator.markSent (src/services/notification-deduplicator.ts:137:12) + at Object. (src/services/notification-deduplicator.test.ts:190:9) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "6ca84ad0"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "697e7796-0ec1-4523-87a8-18639feee515", +"eventId": "event-1", "requestId": "697e7796-0ec1-4523-87a8-18639feee515"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   109 | await Promise.resolve(); +  110 | await Promise.resolve(); + > 111 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  112 | +  113 | queue.stop(); +  114 | }); + + at Object. (src/services/notification-retry-queue.test.ts:111:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   128 | await Promise.resolve(); +  129 | + > 130 | expect(queue.size()).toBe(0); +  | ^ +  131 | queue.stop(); +  132 | }); +  133 | + + at Object. (src/services/notification-retry-queue.test.ts:130:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4950.209477735045, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:22:43.232Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   146 | await Promise.resolve(); +  147 | + > 148 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  149 | 'Retry succeeded', +  150 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  151 | ); + + at Object. (src/services/notification-retry-queue.test.ts:148:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   170 | await Promise.resolve(); +  171 | await Promise.resolve(); + > 172 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  173 | +  174 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  175 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:172:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   198 | await Promise.resolve(); +  199 | + > 200 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  201 | 'Retry failed, scheduling next attempt', +  202 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  203 | ); + + at Object. (src/services/notification-retry-queue.test.ts:200:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   225 | jest.advanceTimersByTime(100); +  226 | await flush(); + > 227 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  228 | +  229 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  230 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:227:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   257 | await Promise.resolve(); +  258 | + > 259 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  260 | 'Notification permanently failed after max retries', +  261 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  262 | ); + + at Object. (src/services/notification-retry-queue.test.ts:259:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   297 | await Promise.resolve(); +  298 | + > 299 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  300 | queue.stop(); +  301 | }); +  302 | }); + + at Object. (src/services/notification-retry-queue.test.ts:299:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   349 | +  350 | // The notification fn was called exactly 3 times + > 351 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  352 | +  353 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  354 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:351:30) + +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/tests/api-versioning.test.ts + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/api/template-routes.test.ts (6.703 s) + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:85:33) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:107:33) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:117:33) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:137:33) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleCreateTemplate (src/api/template-routes.ts:101:27) + at handleTemplateRoutes (src/api/template-routes.ts:432:11) + at Object. (src/api/template-routes.test.ts:153:33) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:201:33) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:215:33) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleUpdateTemplate (src/api/template-routes.ts:266:27) + at handleTemplateRoutes (src/api/template-routes.ts:468:11) + at Object. (src/api/template-routes.test.ts:226:33) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:237:33) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:247:33) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: Cannot read properties of undefined (reading 'content-length') + +   62 | +  63 | // If request explicitly specifies empty body (Content-Length: 0), allow it + > 64 | if (req.headers['content-length'] +=== '0') { +  | ^ +  65 | return true; +  66 | } +  67 | + + at validateContentType (src/middleware/content-type.ts:64:18) + at handleRenderTemplate (src/api/template-routes.ts:339:27) + at handleTemplateRoutes (src/api/template-routes.ts:444:11) + at Object. (src/api/template-routes.test.ts:261:33) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/tests/template-api-integration.test.ts (5.865 s) + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/webhook-delivery-service.test.ts (5.619 s) + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:79:22) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:89:22) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:98:7) + + ● WebhookDeliveryService › successful delivery (2xx) › passes the payload and options to sendWebhook + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:109:7) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:127:24 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:138:7) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at src/services/webhook-delivery-service.test.ts:156:24 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:167:7) + + ● WebhookDeliveryService › request timeout (AbortError) › returns success: false when the request times out + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:183:22) + + ● WebhookDeliveryService › request timeout (AbortError) › logs a warn for timeouts so the caller knows to retry + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:193:7) + + ● WebhookDeliveryService › request timeout (AbortError) › includes the configured timeout in the errorReason + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:205:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › returns success: false on a generic network +error + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:217:22) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › logs an error for non-timeout network failures + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:226:7) + + ● WebhookDeliveryService › network / unexpected errors (retryable) › handles non-Error thrown values gracefully + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:237:22) + + ● WebhookDeliveryService › default options › uses the instance-level timeout when none provided per-call + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:251:7) + + ● WebhookDeliveryService › default options › merges instance headers with per-call headers + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:264:7) + + ● WebhookDeliveryService › failure classification › labels a request timeout as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:282:22) + + ● WebhookDeliveryService › failure classification › labels a TimeoutError as "timeout" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:292:22) + + ● WebhookDeliveryService › failure classification › labels a generic network error as "network" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:300:22) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:309:22) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:317:22) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:325:22) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792066978432} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792153379781, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792066978432} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792153379781, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "25a9b3a9"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event from ingestion to notification +generation + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should handle multiple events in sequence + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing id + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with invalid ledger + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching wildcard filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching specific filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should reject event not matching filter + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Event Routing and Filtering › should extract event name from topic + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should prevent duplicate notifications + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Notification Deduplication › should allow same event from different contracts + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should handle notification failure gracefully + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Error Handling › should continue processing after individual event failure + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should process events quickly (< 100ms +per event) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should handle registry size limits + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Performance and Resource Management › should clean up resources properly + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should execute in under 2 seconds + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should make zero external network calls + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + + ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should run deterministically (same input = same +output) + + TypeError: deduplicator.clear is not a function + +   54 | eventRegistry.clear(); +  55 | mockTransport.clear(); + > 56 | deduplicator.clear(); +  | ^ +  57 | }); +  58 | +  59 | describe('End-to-End Pipeline', () => { + + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:56:18) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/__tests__/notification-flow-e2e.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\__tests__\notification-flow-e2e.test.ts:302 + it(, async) { } + ^ + + SyntaxError: Unexpected token ',' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/webhook-verifier.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\services\webhook-verifier.test.ts:458 + expect(outcome) { }, : .authenticated + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/tests/events-server.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\tests\events-server.test.ts:11 + headers ? : Record; + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/tests/notification-scheduler-refactored.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\tests\notification-scheduler-refactored.test.ts:271 + const pastLock = notification_fixture_builder_1.NotificationFixtureBuilder.dates.past(1000); + ^ + + SyntaxError: Identifier 'pastLock' has already been declared + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/__tests__/query-performance.test.ts + ● Test suite failed to run + + Cannot find module 'vitest' from 'src/__tests__/query-performance.test.ts' + + > 1 | import { describe, it, expect } +from "vitest"; // if the repo uses jest, DELETE this line +  | ^ +  2 | import { readFileSync, writeFileSync, mkdirSync } from +"node:fs"; +  3 | import { dirname, resolve } from "node:path"; +  4 | import { performance } from "node:perf_hooks"; + + at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/index.js:1031:11) + at Object. (src/__tests__/query-performance.test.ts:1:1) + +FAIL src/database/query-performance.test.ts (11.784 s) + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   44 | }); +  45 | + > 46 | it('uses an indexed plan for pending claim queries', +async () => { +  | ^ +  47 | // Seed enough rows that a full scan would be meaningful +  48 | for (let i = 0; i < +200; i++) { +  49 | await db.run( + + at src/database/query-performance.test.ts:46:3 + at Object. (src/database/query-performance.test.ts:20:1) + + +Test Suites: 51 failed, 1 skipped, 101 passed, 152 of 153 total +Tests: 291 failed, 4 skipped, 2123 passed, 2418 total +Snapshots: 0 total +Time: 385.823 s, estimated 390 s +Ran all test suites. diff --git a/_jsum1.txt b/_jsum1.txt new file mode 100644 index 00000000..1238b635 --- /dev/null +++ b/_jsum1.txt @@ -0,0 +1,299 @@ + +Count Name +----- ---- + 54 ● Console + 27 ● Cannot log after tests are done. Did you forget to wait for something async in your test? + 10 ● Test suite failed to run + 4 ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events no... + 4 ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each + 4 ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + 2 ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with + 2 ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + 2 ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview sta... + 2 ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing + 2 ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage + 2 ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive + 2 ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) + 2 ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=... + 2 ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template u... + 2 ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent + 2 ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + 2 ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with + 2 ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-exi... + 2 ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all + 2 ● Template API Integration Tests › Edge Cases and Security › should handle default values + 2 ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + 2 ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + 2 ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + 2 ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + 2 ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to + 2 ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + 2 ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + 2 ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + 2 ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + 2 ● WebhookDeliveryService › successful delivery (2xx) › passes the payload and options to sendWebhook + 2 ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + 2 ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + 2 ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + 2 ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + 2 ● Template API Integration Tests › Performance and Scalability › should handle large template body + 2 ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + 2 ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + 2 ● Template API Integration Tests › Performance and Scalability › should handle many variables + 2 ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + 2 ● template-routes validation › POST /api/templates › rejects an invalid channelType + 2 ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + 2 ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + 2 ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling... + 2 ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed + 2 ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does... + 2 ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + 2 ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + 2 ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + 2 ● Notification Template System › Template Rendering Integration › should log template usage + 2 ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + 2 ● Notification Template System › Template Rendering Integration › should render template via service + 2 ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + 2 ● Notification Template System › Security Tests › should reject script tags in template + 2 ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + 2 ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + 2 ● Notification Template System › Security Tests › should prevent prototype pollution + 2 ● API Route Versioning (#386) › /api/v1/events response has events array + 2 ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + 2 ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + 2 ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + 2 ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + 2 ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in t... + 2 ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + 2 ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent + 2 ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + 2 ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + 2 ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + 2 ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + 2 ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + 2 ● template-routes validation › POST /api/templates/render › rejects a non-object context + 2 ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniq... + 2 ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is g... + 2 ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid... + 2 ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + 2 ● events server logging paths › webhook signature failures never log the secret or signature + 2 ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + 2 ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + 2 ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should run deterministically (same input = same + 2 ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + 2 ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should execute in under 2 seconds + 2 ● Notification Pipeline Smoke Test › Smoke Test Meta-Validation › should make zero external network calls + 2 ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + 2 ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + 2 ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + 2 ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + 2 ● Notification Pipeline Smoke Test › Performance and Resource Management › should clean up resources properly + 2 ● Notification Pipeline Smoke Test › Notification Deduplication › should prevent duplicate notifications + 2 ● Notification Pipeline Smoke Test › Notification Deduplication › should allow same event from different contr... + 2 ● Notification Pipeline Smoke Test › Event Routing and Filtering › should reject event not matching filter + 2 ● Notification Pipeline Smoke Test › Event Routing and Filtering › should extract event name from topic + 2 ● Notification Pipeline Smoke Test › Performance and Resource Management › should process events quickly (< 100ms + 2 ● Notification Pipeline Smoke Test › Performance and Resource Management › should handle registry size limits + 2 ● Notification Pipeline Smoke Test › Error Handling › should handle notification failure gracefully + 2 ● Notification Pipeline Smoke Test › Error Handling › should continue processing after individual event failure + 2 ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + 2 ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + 2 ● handleArchiveRequest › GET /api/archive returns paginated results + 2 ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + 2 ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + 2 ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + 2 ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + 2 ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + 2 ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + 2 ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + 2 ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for + 2 ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a + 2 ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + 2 ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + 2 ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change + 2 ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status + 2 ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp fr... + 2 ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented inste... + 2 ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching specific filter + 2 ● WebhookDeliveryService › failure classification › labels a generic network error as "network" + 2 ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + 2 ● WebhookDeliveryService › failure classification › labels a request timeout as "timeout" + 2 ● WebhookDeliveryService › failure classification › labels a TimeoutError as "timeout" + 2 ● CleanupService › calling start twice does not create duplicate intervals + 2 ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service + 2 ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + 2 ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + 2 ● WebhookDeliveryService › default options › merges instance headers with per-call headers + 2 ● WebhookDeliveryService › request timeout (AbortError) › logs a warn for timeouts so the caller knows to retry + 2 ● WebhookDeliveryService › request timeout (AbortError) › includes the configured timeout in the errorReason + 2 ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal + 2 ● WebhookDeliveryService › request timeout (AbortError) › returns success: false when the request times out + 2 ● WebhookDeliveryService › network / unexpected errors (retryable) › handles non-Error thrown values gracefully + 2 ● WebhookDeliveryService › default options › uses the instance-level timeout when none provided per-call + 2 ● WebhookDeliveryService › network / unexpected errors (retryable) › returns success: false on a generic network + 2 ● WebhookDeliveryService › network / unexpected errors (retryable) › logs an error for non-timeout network fai... + 2 ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and + 2 ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event from ingestion to notification + 2 ● Notification Pipeline Smoke Test › End-to-End Pipeline › should handle multiple events in sequence + 2 ● Config validation › loads default values when optional environment variables are omitted + 2 ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp + 2 ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing topic + 2 ● Notification Pipeline Smoke Test › Event Routing and Filtering › should accept event matching wildcard filter + 2 ● Notification Pipeline Smoke Test › Event Validation › should reject event with missing id + 2 ● Notification Pipeline Smoke Test › Event Validation › should reject event with invalid ledger + 2 ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + 2 ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 an... + 2 ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + 2 ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is + 2 ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least + 2 ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient fai... + 2 ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + 2 ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and... + 2 ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent fai... + 2 ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing... + 2 ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + 2 ● Execution Metrics Deduplication › should return retry distribution breakdown + 2 ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + 2 ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + 2 ● Execution Metrics Deduplication › should calculate accurate average durations + 2 ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + 2 ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry + 2 ● RetryScheduler integration › marks notification FAILED when max retries exhausted + 2 ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + 2 ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks ... + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the... + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as ... + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two lis... + 2 ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once + 2 ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + 2 ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + 2 ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + 2 ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + 2 ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + 2 ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with + 2 ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + 2 ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + 2 ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + 2 ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + 2 ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + 2 ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + 2 ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + 2 ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + 2 ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + 2 ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + 2 ● payload integrity — repository and scheduler integration › stored hash matches the payload + 2 ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without trunca... + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + 2 ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own + 2 ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + 2 ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured c... + 2 ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + 2 ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + 2 ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + 2 ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + 2 ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + 2 ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + 2 ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + 2 ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different cont... + 2 ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook + 2 ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures + 2 ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed + 2 ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + 2 ● Stress Test Suite: Notification Processing › 6. Discord Notification Service Under Load › should handle 500 + 2 ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — + 2 ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + 2 ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + 2 ● Durable notification queue › retains the terminal processing state of a permanently failed job + 2 ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery ... + 2 ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the + 2 ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel + 2 ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload + 2 ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even whe... + 2 ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers w... + 2 ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are... + 2 ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different + 2 ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after + 2 ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + 2 ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, + 2 ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + 2 ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + 2 ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + 2 ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + 2 ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success + 2 ● Notification Template System › TemplateRenderer › should render nested properties + 2 ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + 2 ● Notification Template System › TemplateRenderer › should render simple variable + 2 ● Notification Template System › TemplateRenderer › should render multiple variables + 2 ● NotificationDeduplicator › duplicate prevention across repeated events › allows notifications for unique events + 2 ● NotificationDeduplicator › duplicate prevention across repeated events › treats same event id on different + 2 ● NotificationDeduplicator › size › returns the correct count after multiple unique fingerprints + 2 ● NotificationDeduplicator › duplicate prevention across repeated events › prevents a second notification for the + 2 ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and + 2 ● NotificationRetryQueue › processQueue › logs success on a successful retry + 2 ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + 2 ● NotificationRetryQueue › processQueue › retries a notification after the base delay + 2 ● NotificationRetryQueue › processQueue › removes the item from the queue on success + 2 ● Notification Template System › TemplateValidator › should validate channel-specific requirements + 2 ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + 2 ● Notification Template System › TemplateValidator › should detect script injection + 2 ● Notification Template System › TemplateValidator › should validate unique key format + 2 ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + 2 ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + 2 ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + 2 ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + 2 ● Notification Template System › TemplateService - CRUD Operations › should update template + 2 ● Notification Template System › TemplateRenderer › should extract variables from template + 2 ● Notification Template System › TemplateRenderer › should use default values + 2 ● Notification Template System › TemplateRenderer › should escape HTML by default + 2 ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + 2 ● Notification Template System › TemplateRenderer › should validate context + 2 ● Notification Template System › TemplateValidator › should reject empty variable + 2 ● Notification Template System › TemplateValidator › should reject invalid variable name + 2 ● Notification Template System › TemplateValidator › should validate correct template + 2 ● Notification Template System › TemplateValidator › should detect unclosed brackets + 2 ● GET /health › returns the configured network without exposing sensitive configuration + 2 ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + 2 ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanentl... + 2 ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + 2 ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + 2 ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + 2 ● POST /api/webhooks › rejects when signature header is missing + 2 ● POST /api/webhooks › rejects a webhook with an expired timestamp + 2 ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + 2 ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) + 2 ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + 2 ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without ... + 2 ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification... + 2 ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliverySer... + 2 ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure + 2 ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client error... + 2 ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + 2 ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolu... + 2 ● NotificationDeduplicator › markSent › does not increment size when marking the same fingerprint twice + 2 ● NotificationDeduplicator › markSent › evicts the oldest entry when the cache is full + 2 ● NotificationDeduplicator › isDuplicate › returns false for a different fingerprint after another is marked sent + 2 ● NotificationDeduplicator › markSent › increments size after marking a new fingerprint + 2 ● NotificationDeduplicator › markSent › logs a warning when evicting due to full cache + 2 ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + 2 ● NotificationDeduplicator › time window and metrics › counts evicted and expired entries in metrics + 2 ● NotificationDeduplicator › markSent › does not evict when cache has room + 2 ● NotificationDeduplicator › time window and metrics › allows the same fingerprint after the deduplication window + 2 ● POST /api/webhooks › rejects when no webhook secrets are configured + 2 ● POST /api/webhooks › logs authentication failures with structured context + 2 ● POST /api/webhooks › rejects when key-id header is missing + 2 ● POST /api/webhooks › rejects when key-id is unknown + 2 ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice re... + 2 ● generateFingerprint › combines contractAddress and eventId + 2 ● NotificationDeduplicator › isDuplicate › returns true for a fingerprint that has been marked sent + 2 ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, + 2 ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a + + diff --git a/_jsum2.txt b/_jsum2.txt new file mode 100644 index 00000000..7666dc19 --- /dev/null +++ b/_jsum2.txt @@ -0,0 +1,65 @@ + +Count Name +----- ---- + 130 expect(received).toBe(expected) // Object.is equality + 120 thrown: "Exceeded timeout of N ms for a test. + 100 COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":N,\"code\":\"SQLITE_ERROR\"}"} + 92 Expected: N + 82 Received: N + 66 SQLITE_ERROR: no such table: template_usage_log + 58 TypeError: (N , webhook_sender_N.isWebhookTimeoutError) is not a function + 56 SQLITE_ERROR: no such column: unique_key + 40 TypeError: deduplicator.clear is not a function + 34 Received number of calls: N + 34 expect(jest.fn()).toHaveBeenCalledTimes(expected) + 34 Expected number of calls: N + 33 template_usage_log","params":[],"error":"{\"errno\":N,\"code\":\"SQLITE_ERROR\"}"} + 28 TypeError: logger_N.default.debug is not a function + 28 expect(notificationFn)m.mtoHaveBeenCalledTimes(mNm)m;m + 22 TypeError: Cannot read properties of undefined (reading 'content-length') + 18 expect(jest.fn()).toHaveBeenCalledWith(...expected) + 14 Received: undefined + 12 Received length: N + 12 Received: "DEAD_LETTERED" + 12 SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + 12 Expected length: N + 12 Expected: false + 12 Expected: "FAILED" + 12 expect(received).toHaveLength(expected) + 10 Received: true + 8 Jest encountered an unexpected token + 8 thrown: "Exceeded timeout of N ms for a hook. + 8 + Received + N + 6 expect(received).toHaveProperty(path) + 6 mm>mmm N |m expect(mockLoggerm.mwarn)m.mtoHaveBeenCalledWith( + 6 TypeError: res.setHeader is not a function + 6 - Expected - N + 6 Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, + 6 Received path: [] + 6 expect(received).toEqual(expected) // deep equality + 5 {"error":"{\"errno\":N,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + 4 mm>mmm N |m expect(loggerm.mwarn)m.mtoHaveBeenCalledWith( + 4 m N |m expect(loggerm.merror)m.mtoHaveBeenCalledWith( + 4 Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": N, "delayMs": N, "eventId": + 4 Expected: "Retry succeeded", ObjectContaining {"attempt": N, "eventId": "evt-ok"} + 4 mm>mmm N |m expect(loggerm.minfo)m.mtoHaveBeenCalledWith( + 4 Received + 4 Received value: {"data": {"count": N, "events": []}, "success": true} + 4 {"sql":"END","params":[],"error":"{\"errno\":N,\"code\":\"SQLITE_ERROR\"}"} + 4 expect(received).toContain(expected) // indexOf + 4 Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", + 4 mm>mmm N |m expect(loggerm.merror)m.mtoHaveBeenCalledWith( + 4 Expected path: "events" + 4 Received array: [] + 4 Received: false + 4 expect(fetchMock)m.mtoHaveBeenCalledTimes(mNm)m;m + 4 Expected: true + 4 SyntaxError: Unexpected token ':' + 3 statement expected. + 3 msrc/tests/events-server.test.tsm:mNm:mNm - merrormm TSN: m',' expected. + 2 Expected path: "records" + 2 RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":N,\"code\":\"SQLITE_ERROR\"}"} + 2 TypeError: Cannot read properties of undefined (reading 'fromString') + 2 Received string: "Cannot read properties of undefined (reading 'get')" + + diff --git a/_ne.txt b/_ne.txt new file mode 100644 index 00000000..40e5d67a --- /dev/null +++ b/_ne.txt @@ -0,0 +1,116 @@ +import * as StellarSDK from '@stellar/stellar-sdk'; +import { ExpirationConfig } from '../types'; +import logger from '../utils/logger'; + +/** + * NotificationExpirationService handles expiration checks for notifications. + * + * This service: + * - Checks if notifications have exceeded their expiration timestamp + * - Supports configurable default expiration (24 hours by default) + * - Supports per-event-type expiration overrides + * - Can be disabled via configuration for backward compatibility + */ +export class NotificationExpirationService { + private config: ExpirationConfig; + + constructor(config: ExpirationConfig) { + this.config = config; + } + + /** + * Check if a notification has expired based on its receivedAt timestamp + * and the appropriate expiration time. + * + * @param event - The blockchain event to check + * @returns true if the event has expired, false otherwise + */ + isExpired(event: StellarSDK.rpc.Api.EventResponse): boolean { + // If expiration is disabled, nothing is ever expired + if (!this.config.enabled) { + return false; + } + + // Get the expiration time in milliseconds for this event + const expirationTimeMs = this.getExpirationTime(); + + // Calculate when this event should expire + // receivedAt is in milliseconds (Unix timestamp) + const expiresAtMs = event.receivedAt + expirationTimeMs; + + // Check if current time exceeds the expiration time + const currentTimeMs = Date.now(); + return currentTimeMs > expiresAtMs; + } + + /** + * Determine if an event should be processed based on expiration status. + * + * This is the main entry point for checking if an event is still valid. + * + * @param event - The blockchain event to check + * @param eventType - Optional event type for per-type expiration lookup + * @returns true if the event should be processed, false if expired + */ + shouldProcess( + event: StellarSDK.rpc.Api.EventResponse, + eventType?: string + ): boolean { + // If expiration is disabled, always process + if (!this.config.enabled) { + return true; + } + + const expired = this.isExpired(event); + + if (expired) { + logger.warn('Event skipped due to expiration', { + eventId: event.id, + receivedAt: event.receivedAt, + eventType, + currentTime: Date.now(), + }); + } + + return !expired; + } + + /** + * Get the expiration time in milliseconds for a given event type. + * + * Uses per-event-type configuration if available, otherwise uses default. + * + * @param eventType - Optional event type to look up specific expiration time + * @returns Expiration time in milliseconds + */ + getExpirationTime(eventType?: string): number { + // If an event type is provided, check for per-type expiration + if (eventType && this.config.perEventTypeExpiration) { + const perTypeExpiration = this.config.perEventTypeExpiration[eventType]; + if (perTypeExpiration !== undefined) { + return perTypeExpiration; + } + } + + // Return default expiration + return this.config.defaultExpirationMs; + } + + /** + * Get the current configuration. + * + * @returns The expiration configuration + */ + getConfig(): ExpirationConfig { + return this.config; + } + + /** + * Update the configuration at runtime. + * + * @param config - New expiration configuration + */ + setConfig(config: ExpirationConfig): void { + this.config = config; + } +} diff --git a/_nfdiff.txt b/_nfdiff.txt new file mode 100644 index 00000000..325c2594 --- /dev/null +++ b/_nfdiff.txt @@ -0,0 +1,155 @@ +diff --git a/listener/src/__tests__/notification-flow-e2e.test.ts b/listener/src/__tests__/notification-flow-e2e.test.ts +index f97be55..117c9c4 100644 +--- a/listener/src/__tests__/notification-flow-e2e.test.ts ++++ b/listener/src/__tests__/notification-flow-e2e.test.ts +@@ -28,4 +28,5 @@ import { BackpressureController } from '../services/backpressure-controller'; + import { BackpressureMonitor } from '../services/backpressure-monitor'; + import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-builder'; ++import { resetWorkerManager } from '../services/worker-manager'; + import { NotificationStatus, NotificationType } from '../types/scheduled-notification'; + +@@ -119,4 +120,5 @@ describe('1. Notification creation', () => { + + beforeEach(async () => { ++ resetWorkerManager(); + jest.clearAllMocks(); + jest.useFakeTimers(); +@@ -166,4 +168,19 @@ describe('1. Notification creation', () => { + ).rejects.toThrow('targetRecipient is required'); + }); ++ afterEach(async () => { ++ await scheduler.stop(); ++ }); ++ ++ async function waitForSchedulerPolls(ms = 300): Promise { ++ await new Promise((resolve) => setTimeout(resolve, ms)); ++ } ++ ++ function futureExecuteAt(offsetMs = 10000): Date { ++ return new Date(Date.now() + offsetMs); ++ } ++ ++ describe('Complete notification lifecycle', () => { ++ it('should create, process, and deliver a notification', async () => { ++ const executeAt = futureExecuteAt(50); + + it('rejects a notification with an invalid payload', async () => { +@@ -228,4 +245,6 @@ describe('1. Notification creation', () => { + expect(stats.pending).toBe(1); + }); ++ await scheduler.start(); ++ await waitForSchedulerPolls(400); + + it('rejects a duplicate idempotency key used with a different payload', async () => { +@@ -262,4 +281,20 @@ describe('1. Notification creation', () => { + expect(row!.status).toBe(NotificationStatus.CANCELLED); + }); ++ ++ it('cancels a scheduled notification with a reason', async () => { ++ const id = await api.scheduleNotification({ ++ payload: { message: 'cancel me with reason' }, ++ notificationType: NotificationType.DISCORD, ++ targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, ++ executeAt: new Date('2026-06-24T13:00:00.000Z'), ++ }); ++ ++ const cancelled = await api.cancelNotification(id, 'Admin override'); ++ expect(cancelled).toBe(true); ++ ++ const row = await repository.getById(id); ++ expect(row!.status).toBe(NotificationStatus.CANCELLED); ++ expect(row!.cancellationReason).toBe('Admin override'); ++ }); + }); + +@@ -309,4 +344,24 @@ describe('2. Delivery workflows', () => { + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:00:02.000Z'), ++ it('should log execution attempts for audit trail', async () => { ++ const id = await api.scheduleNotification({ ++ payload: { message: 'Test notification' }, ++ notificationType: NotificationType.DISCORD, ++ targetRecipient: 'https://discord.com/webhook', ++ executeAt: futureExecuteAt(50), ++ }); ++ ++ await scheduler.start(); ++ await waitForSchedulerPolls(400); ++ ++ const logs = await db.all( ++ 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', ++ [id] ++ ); ++ ++ expect(logs).toHaveLength(1); ++ expect(logs[0].status).toBe('SUCCESS'); ++ expect(logs[0].execution_attempt).toBe(1); ++ expect(logs[0].scheduled_notification_id).toBe(id); + }); + +@@ -326,4 +381,9 @@ describe('2. Delivery workflows', () => { + executeAt: new Date('2026-06-24T12:10:00.000Z'), // 10 min future + }); ++ describe('Idempotency handling', () => { ++ it('should return cached response for duplicate requests with same idempotency key', async () => { ++ const executeAt = futureExecuteAt(); ++ const payload = { message: 'Unique message' }; ++ const idempotencyKey = 'test-idempotency-key-1'; + + await scheduler.start(); +@@ -346,4 +406,7 @@ describe('2. Delivery workflows', () => { + await jest.advanceTimersByTimeAsync(200); + expect((await repository.getById(id))!.status).toBe(NotificationStatus.PENDING); ++ it('should reject duplicate requests with different payload', async () => { ++ const executeAt = futureExecuteAt(); ++ const idempotencyKey = 'test-idempotency-key-2'; + + jest.setSystemTime(new Date('2026-06-24T12:05:01.000Z')); +@@ -364,4 +427,7 @@ describe('2. Delivery workflows', () => { + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); ++ it('should clean up expired idempotency keys', async () => { ++ const executeAt = futureExecuteAt(); ++ const idempotencyKey = 'test-idempotency-key-3'; + + const logs = await db.all( +@@ -431,4 +497,15 @@ describe('3. Retry scenarios', () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); ++ await db.run( ++ 'UPDATE idempotency_keys SET expires_at = ? WHERE idempotency_key = ?', ++ [new Date(Date.now() - 1000).toISOString(), idempotencyKey] ++ ); ++ ++ const cleanupCount = await idempotencyService.cleanupExpiredKeys(); ++ expect(cleanupCount).toBeGreaterThanOrEqual(1); ++ ++ stats = await idempotencyRepo.getStats(); ++ expect(stats.expired).toBeGreaterThanOrEqual(0); ++ }); + }); + +@@ -535,4 +612,9 @@ describe('3. Retry scenarios', () => { + expect(sendMock).not.toHaveBeenCalled(); + }); ++ describe('Integration tests', () => { ++ it('should handle high-volume notification creation with idempotency', async () => { ++ const executeAt = futureExecuteAt(); ++ const payload = { message: 'Batch test' }; ++ const idempotencyKey = 'batch-idempotency-1'; + + it('prevents concurrent duplicate processing via distributed lock', async () => { +@@ -811,4 +893,6 @@ describe('5. Backpressure and idempotency integration', () => { + + afterEach(() => jest.useRealTimers()); ++ it('should maintain audit trail through complete lifecycle', async () => { ++ const executeAt = futureExecuteAt(50); + + it('activates backpressure when queue exceeds saturation threshold', () => { +@@ -821,4 +905,6 @@ describe('5. Backpressure and idempotency integration', () => { + expect(metrics.targetThroughputPerSec).toBe(10); + }); ++ await scheduler.start(); ++ await waitForSchedulerPolls(400); + + it('calculates a positive processing delay under backpressure', () => { diff --git a/_nflines.txt b/_nflines.txt new file mode 100644 index 00000000..e69de29b diff --git a/_nsr1cf.txt b/_nsr1cf.txt new file mode 100644 index 00000000..d6ddc353 --- /dev/null +++ b/_nsr1cf.txt @@ -0,0 +1,424 @@ +/** + * Refactored NotificationScheduler Tests + * + * Uses NotificationFixtureBuilder to eliminate duplicate fixtures + * and provide deterministic, type-safe test data. + */ + +import { Database } from '../database/database'; +import { ScheduledNotificationRepository } from '../services/scheduled-notification-repository'; +import { NotificationScheduler } from '../services/notification-scheduler'; +import { NotificationAPI } from '../services/notification-api'; +import { NotificationType, NotificationStatus } from '../types/scheduled-notification'; +import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-builder'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('NotificationScheduler (Refactored)', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let scheduler: NotificationScheduler; + let api: NotificationAPI; + + const testDbPath = './data/test-notifications-refactored.db'; + + beforeAll(async () => { + const dbDir = path.dirname(testDbPath); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + if (fs.existsSync(testDbPath)) { + fs.unlinkSync(testDbPath); + } + + db = new Database(testDbPath); + await db.initialize(); + repository = new ScheduledNotificationRepository(db); + api = new NotificationAPI(repository); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(testDbPath)) { + fs.unlinkSync(testDbPath); + } + }); + + beforeEach(async () => { + await db.run('DELETE FROM notification_execution_log'); + await db.run('DELETE FROM scheduled_notifications'); + }); + + describe('ScheduledNotificationRepository', () => { + test('should create a scheduled notification', async () => { + // ✅ Using fixture builder - no more hardcoded fixture + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .build(); + + const id = await repository.create(input); + expect(id).toBeGreaterThan(0); + + const notification = await repository.getById(id); + expect(notification).toBeDefined(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.retryCount).toBe(0); + }); + + test('should fetch and lock pending notifications', async () => { + // ✅ Using deterministic date - always same result + const input1 = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() // Past date for immediate processing + .withPayload({ message: 'Test 1' }) + .build(); + + const input2 = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withPayload({ message: 'Test 2' }) + .build(); + + await repository.create(input1); + await repository.create(input2); + + const processorId = 'test-processor-1'; + const notifications = await repository.fetchAndLockPendingNotifications( + processorId, + 30000, + 10 + ); + + expect(notifications.length).toBe(2); + expect(notifications[0].status).toBe(NotificationStatus.PROCESSING); + expect(notifications[0].processorId).toBe(processorId); + expect(notifications[0].lockExpiresAt).toBeDefined(); + }); + + test('should prevent race conditions with distributed locking', async () => { + // ✅ Single fixture creation, easy to read + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + await repository.create(input); + + const processor1 = await repository.fetchAndLockPendingNotifications( + 'processor-1', + 30000, + 10 + ); + const processor2 = await repository.fetchAndLockPendingNotifications( + 'processor-2', + 30000, + 10 + ); + + expect(processor1.length).toBe(1); + expect(processor2.length).toBe(0); // Should not pick up locked notification + }); + + test('should recover stale locks', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.processorId).toBeNull(); + }); + + test('should mark notification as completed', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + const id = await repository.create(input); + await repository.markAsCompleted(id); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.COMPLETED); + expect(notification!.processingCompletedAt).toBeDefined(); + }); + + test('should retry failed notification', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + const error = new Error('Test error'); + await repository.markAsFailedOrRetry(id, error, 0, 3); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); // Should retry + expect(notification!.retryCount).toBe(1); + expect(notification!.lastError).toBe('Test error'); + }); + + test('should mark as failed after max retries', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + const error = new Error('Test error'); + await repository.markAsFailedOrRetry(id, error, 3, 3); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.retryCount).toBe(4); + }); + + test('should cancel pending notification', async () => { + // ✅ Future execution - easy to express intent + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forFutureExecution() + .build(); + + const id = await repository.create(input); + const cancelled = await repository.cancel(id); + + expect(cancelled).toBe(true); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.CANCELLED); + }); + + test('should get statistics', async () => { + // ✅ Create multiple with different scenarios + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build() + ); + + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() // Overdue + .build() + ); + + const stats = await repository.getStats(); + expect(stats.pending).toBe(2); + expect(stats.overdue).toBe(1); + }); + + test('should increment retry_count and log attempt on lock recovery', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.retryCount).toBe(1); + expect(notification!.lastError).toBe('Lock expired/Processor timeout'); + + // Verify execution log + const logs = await db.all('SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id]); + expect(logs.length).toBe(1); + expect(logs[0].status).toBe('RETRY'); + expect(logs[0].error_message).toBe('Lock expired/Processor timeout'); + }); + + test('should mark as failed if retry_count reaches max_retries on lock recovery', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(1) + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.retryCount).toBe(1); + + // Verify execution log + const logs = await db.all('SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id]); + expect(logs.length).toBe(1); + expect(logs[0].status).toBe('FAILED'); + }); + + test('should return correct statistics accounting for stale locks', async () => { + // 1. Create a notification in the future (pending, not overdue) + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build() + ); + + // 2. Create a notification in the past that is currently PROCESSING but lock is expired + const staleId = await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + staleId, + ]); + + // 3. Create a notification in the past (overdue, pending) - created AFTER locking to remain in PENDING status + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); + + // Get stats BEFORE recovery + const stats = await repository.getStats(); + + expect(stats.pending).toBe(3); + expect(stats.overdue).toBe(2); + expect(stats.processing).toBe(0); + }); + }); + + describe('NotificationAPI', () => { + test('should schedule notification via API', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .withPayload({ message: 'API test' }) + .build(); + + const id = await api.scheduleNotification(input); + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification).toBeDefined(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + }); + + test('should reject past execution time', async () => { + // ✅ Deterministic past date + const pastInput = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withExecuteAt(NotificationFixtureBuilder.dates.past(60000)) + .build(); + + await expect( + api.scheduleNotification(pastInput) + ).rejects.toThrow('executeAt must be a future timestamp'); + }); + + test('should schedule Discord notification', async () => { + // ✅ Using deterministic constants + const webhookUrl = NotificationFixtureBuilder.constants.webhookUrl; + const executeAt = new Date(Date.now() + 3600000); + + const id = await api.scheduleDiscordNotification( + webhookUrl, + { content: 'Hello World' }, + executeAt, + { priority: 1, maxRetries: 5 } + ); + + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification!.priority).toBe(1); + expect(notification!.maxRetries).toBe(5); + }); + + test('should schedule different notification types', async () => { + // ✅ Test all types easily + const types = [ + NotificationType.DISCORD, + NotificationType.EMAIL, + NotificationType.WEBHOOK, + NotificationType.SMS, + ]; + + for (const type of types) { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .withType(type) + .withExecuteAt(new Date(Date.now() + 3600000)) + .build(); + + const id = await api.scheduleNotification(input); + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification!.notificationType).toBe(type); + } + }); + + test('should get statistics via API', async () => { + await api.scheduleNotification( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build() + ); + + const stats = await api.getStatistics(); + expect(stats.pending).toBeGreaterThan(0); + }); + }); +}); diff --git a/_nsr_1cf.txt b/_nsr_1cf.txt new file mode 100644 index 00000000..d6ddc353 --- /dev/null +++ b/_nsr_1cf.txt @@ -0,0 +1,424 @@ +/** + * Refactored NotificationScheduler Tests + * + * Uses NotificationFixtureBuilder to eliminate duplicate fixtures + * and provide deterministic, type-safe test data. + */ + +import { Database } from '../database/database'; +import { ScheduledNotificationRepository } from '../services/scheduled-notification-repository'; +import { NotificationScheduler } from '../services/notification-scheduler'; +import { NotificationAPI } from '../services/notification-api'; +import { NotificationType, NotificationStatus } from '../types/scheduled-notification'; +import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-builder'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('NotificationScheduler (Refactored)', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let scheduler: NotificationScheduler; + let api: NotificationAPI; + + const testDbPath = './data/test-notifications-refactored.db'; + + beforeAll(async () => { + const dbDir = path.dirname(testDbPath); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + if (fs.existsSync(testDbPath)) { + fs.unlinkSync(testDbPath); + } + + db = new Database(testDbPath); + await db.initialize(); + repository = new ScheduledNotificationRepository(db); + api = new NotificationAPI(repository); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(testDbPath)) { + fs.unlinkSync(testDbPath); + } + }); + + beforeEach(async () => { + await db.run('DELETE FROM notification_execution_log'); + await db.run('DELETE FROM scheduled_notifications'); + }); + + describe('ScheduledNotificationRepository', () => { + test('should create a scheduled notification', async () => { + // ✅ Using fixture builder - no more hardcoded fixture + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .build(); + + const id = await repository.create(input); + expect(id).toBeGreaterThan(0); + + const notification = await repository.getById(id); + expect(notification).toBeDefined(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.retryCount).toBe(0); + }); + + test('should fetch and lock pending notifications', async () => { + // ✅ Using deterministic date - always same result + const input1 = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() // Past date for immediate processing + .withPayload({ message: 'Test 1' }) + .build(); + + const input2 = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withPayload({ message: 'Test 2' }) + .build(); + + await repository.create(input1); + await repository.create(input2); + + const processorId = 'test-processor-1'; + const notifications = await repository.fetchAndLockPendingNotifications( + processorId, + 30000, + 10 + ); + + expect(notifications.length).toBe(2); + expect(notifications[0].status).toBe(NotificationStatus.PROCESSING); + expect(notifications[0].processorId).toBe(processorId); + expect(notifications[0].lockExpiresAt).toBeDefined(); + }); + + test('should prevent race conditions with distributed locking', async () => { + // ✅ Single fixture creation, easy to read + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + await repository.create(input); + + const processor1 = await repository.fetchAndLockPendingNotifications( + 'processor-1', + 30000, + 10 + ); + const processor2 = await repository.fetchAndLockPendingNotifications( + 'processor-2', + 30000, + 10 + ); + + expect(processor1.length).toBe(1); + expect(processor2.length).toBe(0); // Should not pick up locked notification + }); + + test('should recover stale locks', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.processorId).toBeNull(); + }); + + test('should mark notification as completed', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + const id = await repository.create(input); + await repository.markAsCompleted(id); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.COMPLETED); + expect(notification!.processingCompletedAt).toBeDefined(); + }); + + test('should retry failed notification', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + const error = new Error('Test error'); + await repository.markAsFailedOrRetry(id, error, 0, 3); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); // Should retry + expect(notification!.retryCount).toBe(1); + expect(notification!.lastError).toBe('Test error'); + }); + + test('should mark as failed after max retries', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + const error = new Error('Test error'); + await repository.markAsFailedOrRetry(id, error, 3, 3); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.retryCount).toBe(4); + }); + + test('should cancel pending notification', async () => { + // ✅ Future execution - easy to express intent + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forFutureExecution() + .build(); + + const id = await repository.create(input); + const cancelled = await repository.cancel(id); + + expect(cancelled).toBe(true); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.CANCELLED); + }); + + test('should get statistics', async () => { + // ✅ Create multiple with different scenarios + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build() + ); + + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() // Overdue + .build() + ); + + const stats = await repository.getStats(); + expect(stats.pending).toBe(2); + expect(stats.overdue).toBe(1); + }); + + test('should increment retry_count and log attempt on lock recovery', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.retryCount).toBe(1); + expect(notification!.lastError).toBe('Lock expired/Processor timeout'); + + // Verify execution log + const logs = await db.all('SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id]); + expect(logs.length).toBe(1); + expect(logs[0].status).toBe('RETRY'); + expect(logs[0].error_message).toBe('Lock expired/Processor timeout'); + }); + + test('should mark as failed if retry_count reaches max_retries on lock recovery', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(1) + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.retryCount).toBe(1); + + // Verify execution log + const logs = await db.all('SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id]); + expect(logs.length).toBe(1); + expect(logs[0].status).toBe('FAILED'); + }); + + test('should return correct statistics accounting for stale locks', async () => { + // 1. Create a notification in the future (pending, not overdue) + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build() + ); + + // 2. Create a notification in the past that is currently PROCESSING but lock is expired + const staleId = await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + staleId, + ]); + + // 3. Create a notification in the past (overdue, pending) - created AFTER locking to remain in PENDING status + await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); + + // Get stats BEFORE recovery + const stats = await repository.getStats(); + + expect(stats.pending).toBe(3); + expect(stats.overdue).toBe(2); + expect(stats.processing).toBe(0); + }); + }); + + describe('NotificationAPI', () => { + test('should schedule notification via API', async () => { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .withPayload({ message: 'API test' }) + .build(); + + const id = await api.scheduleNotification(input); + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification).toBeDefined(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + }); + + test('should reject past execution time', async () => { + // ✅ Deterministic past date + const pastInput = NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .withExecuteAt(NotificationFixtureBuilder.dates.past(60000)) + .build(); + + await expect( + api.scheduleNotification(pastInput) + ).rejects.toThrow('executeAt must be a future timestamp'); + }); + + test('should schedule Discord notification', async () => { + // ✅ Using deterministic constants + const webhookUrl = NotificationFixtureBuilder.constants.webhookUrl; + const executeAt = new Date(Date.now() + 3600000); + + const id = await api.scheduleDiscordNotification( + webhookUrl, + { content: 'Hello World' }, + executeAt, + { priority: 1, maxRetries: 5 } + ); + + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification!.priority).toBe(1); + expect(notification!.maxRetries).toBe(5); + }); + + test('should schedule different notification types', async () => { + // ✅ Test all types easily + const types = [ + NotificationType.DISCORD, + NotificationType.EMAIL, + NotificationType.WEBHOOK, + NotificationType.SMS, + ]; + + for (const type of types) { + const input = NotificationFixtureBuilder + .aScheduledNotificationInput() + .withType(type) + .withExecuteAt(new Date(Date.now() + 3600000)) + .build(); + + const id = await api.scheduleNotification(input); + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification!.notificationType).toBe(type); + } + }); + + test('should get statistics via API', async () => { + await api.scheduleNotification( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build() + ); + + const stats = await api.getStatistics(); + expect(stats.pending).toBeGreaterThan(0); + }); + }); +}); diff --git a/_nsr_diff.txt b/_nsr_diff.txt new file mode 100644 index 00000000..e69de29b diff --git a/_nsr_head.txt b/_nsr_head.txt new file mode 100644 index 00000000..3438b959 --- /dev/null +++ b/_nsr_head.txt @@ -0,0 +1,432 @@ +/** + * Refactored NotificationScheduler Tests + * + * Uses NotificationFixtureBuilder to eliminate duplicate fixtures + * and provide deterministic, type-safe test data. + */ + +import { Database } from '../database/database'; +import { ScheduledNotificationRepository } from '../services/scheduled-notification-repository'; +import { NotificationScheduler } from '../services/notification-scheduler'; +import { NotificationAPI } from '../services/notification-api'; +import { NotificationType, NotificationStatus } from '../types/scheduled-notification'; +import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-builder'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('NotificationScheduler (Refactored)', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let scheduler: NotificationScheduler; + let api: NotificationAPI; + + const testDbPath = './data/test-notifications-refactored.db'; + + beforeAll(async () => { + const dbDir = path.dirname(testDbPath); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + if (fs.existsSync(testDbPath)) { + fs.unlinkSync(testDbPath); + } + + db = new Database(testDbPath); + await db.initialize(); + repository = new ScheduledNotificationRepository(db); + api = new NotificationAPI(repository); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(testDbPath)) { + fs.unlinkSync(testDbPath); + } + }); + + beforeEach(async () => { + await db.run('DELETE FROM notification_execution_log'); + await db.run('DELETE FROM scheduled_notifications'); + }); + + describe('ScheduledNotificationRepository', () => { + test('should create a scheduled notification', async () => { + // ✅ Using fixture builder - no more hardcoded fixture + const input = NotificationFixtureBuilder.aScheduledNotificationInput().build(); + + const id = await repository.create(input); + expect(id).toBeGreaterThan(0); + + const notification = await repository.getById(id); + expect(notification).toBeDefined(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.retryCount).toBe(0); + }); + + test('should fetch and lock pending notifications', async () => { + // ✅ Using deterministic date - always same result + const input1 = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() // Past date for immediate processing + .withPayload({ message: 'Test 1' }) + .build(); + + const input2 = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withPayload({ message: 'Test 2' }) + .build(); + + await repository.create(input1); + await repository.create(input2); + + const processorId = 'test-processor-1'; + const notifications = await repository.fetchAndLockPendingNotifications( + processorId, + 30000, + 10, + ); + + expect(notifications.length).toBe(2); + expect(notifications[0].status).toBe(NotificationStatus.PROCESSING); + expect(notifications[0].processorId).toBe(processorId); + expect(notifications[0].lockExpiresAt).toBeDefined(); + }); + + test('should prevent race conditions with distributed locking', async () => { + // ✅ Single fixture creation, easy to read + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + await repository.create(input); + + const processor1 = await repository.fetchAndLockPendingNotifications( + 'processor-1', + 30000, + 10, + ); + const processor2 = await repository.fetchAndLockPendingNotifications( + 'processor-2', + 30000, + 10, + ); + + expect(processor1.length).toBe(1); + expect(processor2.length).toBe(0); // Should not pick up locked notification + }); + + test('should recover stale locks', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.processorId).toBeNull(); + }); + + test('should mark notification as completed', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .build(); + + const id = await repository.create(input); + await repository.markAsCompleted(id); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.COMPLETED); + expect(notification!.processingCompletedAt).toBeDefined(); + }); + + test('should retry failed notification', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + const error = new Error('Test error'); + await repository.markAsFailedOrRetry(id, error, 0, 3); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); // Should retry + expect(notification!.retryCount).toBe(1); + expect(notification!.lastError).toBe('Test error'); + }); + + test('should mark as failed after max retries', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + const error = new Error('Test error'); + await repository.markAsFailedOrRetry(id, error, 3, 3); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.retryCount).toBe(4); + }); + + test('should cancel pending notification', async () => { + // ✅ Future execution - easy to express intent + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forFutureExecution() + .build(); + + const id = await repository.create(input); + const cancelled = await repository.cancel(id); + + expect(cancelled).toBe(true); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.CANCELLED); + }); + + test('should get statistics', async () => { + // ✅ Create multiple with different scenarios + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build(), + ); + + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() // Overdue + .build(), + ); + + const stats = await repository.getStats(); + expect(stats.pending).toBe(2); + expect(stats.overdue).toBe(1); + }); + + test('should increment retry_count and log attempt on lock recovery', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.PENDING); + expect(notification!.retryCount).toBe(1); + expect(notification!.lastError).toBe('Lock expired/Processor timeout'); + + // Verify execution log + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id], + ); + expect(logs.length).toBe(1); + expect(logs[0].status).toBe('RETRY'); + expect(logs[0].error_message).toBe('Lock expired/Processor timeout'); + }); + + test('should mark as failed if retry_count reaches max_retries on lock recovery', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(1) + .build(); + + const id = await repository.create(input); + + // Lock the notification + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + + // Manually expire the lock + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + id, + ]); + + const recovered = await repository.recoverStaleLocks(); + expect(recovered).toBe(1); + + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.retryCount).toBe(1); + + // Verify execution log + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id], + ); + expect(logs.length).toBe(1); + expect(logs[0].status).toBe('FAILED'); + }); + + test('should return correct statistics accounting for stale locks', async () => { + // 1. Create a notification in the future (pending, not overdue) + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build(), + ); + + // 2. Create a notification in the past (overdue, pending) + const overdueId = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), + ); + + // 3. Create a notification in the past that is currently PROCESSING but lock is expired + const staleId = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), + ); + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run( + `UPDATE scheduled_notifications + SET status = ?, processor_id = ?, lock_expires_at = ?, processing_started_at = ? + WHERE id = ?`, + [ + NotificationStatus.PROCESSING, + 'processor-1', + pastLock.toISOString(), + pastLock.toISOString(), + staleId, + ], + ); + + // 3. Create a notification in the past (overdue, pending) - created AFTER locking to remain in PENDING status + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), + ); + // Lock only the stale notification — fetchAndLock picks up past items by priority order, + // so we lock both past items then restore item 2 to PENDING to isolate the stale case. + await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); + await db.run( + "UPDATE scheduled_notifications SET status = 'PENDING', processor_id = NULL, lock_expires_at = NULL WHERE id = ?", + [overdueId], + ); + const pastLock = NotificationFixtureBuilder.dates.past(1000); + await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ + pastLock.toISOString(), + staleId, + ]); + + // Get stats BEFORE recovery: + // - item 1: PENDING (future) + // - item 2: PENDING (restored, overdue) + // - item 3: PROCESSING with expired lock → getStats adjusts to PENDING + const stats = await repository.getStats(); + + expect(stats.pending).toBe(3); + expect(stats.overdue).toBe(2); + expect(stats.processing).toBe(0); + }); + }); + + describe('NotificationAPI', () => { + test('should schedule notification via API', async () => { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .withPayload({ message: 'API test' }) + .build(); + + const id = await api.scheduleNotification(input); + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification).toBeDefined(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + }); + + test('should reject past execution time', async () => { + // ✅ Deterministic past date + const pastInput = NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withExecuteAt(NotificationFixtureBuilder.dates.past(60000)) + .build(); + + await expect(api.scheduleNotification(pastInput)).rejects.toThrow( + 'executeAt must be a future timestamp', + ); + }); + + test('should schedule Discord notification', async () => { + // ✅ Using deterministic constants + const webhookUrl = NotificationFixtureBuilder.constants.webhookUrl; + const executeAt = new Date(Date.now() + 3600000); + + const id = await api.scheduleDiscordNotification( + webhookUrl, + { content: 'Hello World' }, + executeAt, + { priority: 1, maxRetries: 5 }, + ); + + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification!.priority).toBe(1); + expect(notification!.maxRetries).toBe(5); + }); + + test('should schedule different notification types', async () => { + // ✅ Test all types easily + const types = [ + NotificationType.DISCORD, + NotificationType.EMAIL, + NotificationType.WEBHOOK, + NotificationType.SMS, + ]; + + for (const type of types) { + const input = NotificationFixtureBuilder.aScheduledNotificationInput() + .withType(type) + .withExecuteAt(new Date(Date.now() + 3600000)) + .build(); + + const id = await api.scheduleNotification(input); + expect(id).toBeGreaterThan(0); + + const notification = await api.getNotification(id); + expect(notification!.notificationType).toBe(type); + } + }); + + test('should get statistics via API', async () => { + await api.scheduleNotification( + NotificationFixtureBuilder.aScheduledNotificationInput() + .withExecuteAt(new Date(Date.now() + 3600000)) + .build(), + ); + + const stats = await api.getStatistics(); + expect(stats.pending).toBeGreaterThan(0); + }); + }); +}); diff --git a/_one.txt b/_one.txt new file mode 100644 index 00000000..6593bda3 --- /dev/null +++ b/_one.txt @@ -0,0 +1,42 @@ + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:79:22) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; +  151 | const isDnsBlocked = err instanceof Error +&& err.name === 'DnsRebindingError'; + > 152 | const isTimeout = +isWebhookTimeoutError(err); +  | ^ +  153 | const errorReason = isTimeout +  154 | ? `Webhook request timed out after ${timeoutMs}ms` +  155 | : err instanceof Error + + at WebhookDeliveryService.deliver (src/services/webhook-delivery-service.ts:152:46) + at Object. (src/services/webhook-delivery-service.test.ts:89:22) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + TypeError: (0 , webhook_sender_1.isWebhookTimeoutError) is not a function + +   150 | const durationMs = Date.now() - +startMs; diff --git a/_t1.txt b/_t1.txt new file mode 100644 index 00000000..3ebb2053 --- /dev/null +++ b/_t1.txt @@ -0,0 +1,467 @@ +/** + * Tests for webhook-retry-helper + * + * Validates the bounded retry mechanism for transient API failures. + * + * Coverage: + * - Retryable HTTP failures (429, 500, 502, 503, 504) are retried + * - Transient failures eventually succeed after retry + * - Permanent client errors (400, 401, 403, 404, 422) are NOT retried + * - Network/connection failures are retried + * - Retry attempts are bounded (initial + 2 retries = 3 total attempts max) + * - Delay occurs between retry attempts + * - Retry attempt counts are explicitly validated + * - Success responses are returned immediately without retry + */ + +import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import { sendWebhookWithRetry, classifyWebhookFailure } from './webhook-retry-helper'; + +// --------------------------------------------------------------------------- +// Mock sendWebhook to prevent real HTTP requests +// --------------------------------------------------------------------------- + +jest.mock('./webhook-sender', () => ({ + sendWebhook: jest.fn(), +})); + +import { sendWebhook } from './webhook-sender'; +const mockSendWebhook = sendWebhook as jest.MockedFunction; + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +function makeResponse(status: number, ok = status >= 200 && status < 300): Response { + return { + status, + ok, + statusText: status >= 200 && status < 300 ? 'OK' : 'Error', + text: jest.fn<() => Promise>().mockResolvedValue(''), + } as unknown as Response; +} + +function makeNetworkError(): Error { + return new Error('ECONNREFUSED'); +} + +function makeTimeoutError(): Error { + const err = new Error('The operation was aborted'); + err.name = 'AbortError'; + return err; +} + +const TARGET_URL = 'https://example.com/webhook'; +const PAYLOAD = { event: 'test', data: { key: 'value' } }; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('sendWebhookWithRetry', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + // ── Successful requests ────────────────────────────────────────────────── + + describe('successful requests (2xx)', () => { + it('returns the response immediately on success without retrying', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(200); + expect(result.ok).toBe(true); + expect(mockSendWebhook).toHaveBeenCalledTimes(1); + }); + + it('returns 201 response without retrying', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(201)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(201); + expect(mockSendWebhook).toHaveBeenCalledTimes(1); + }); + }); + + // ── Permanent client errors (no retry) ─────────────────────────────────── + + describe('permanent client errors (4xx) are NOT retried', () => { + it.each([400, 401, 403, 404, 422])( + 'returns HTTP %i immediately without retrying', + async (status) => { + mockSendWebhook.mockResolvedValue(makeResponse(status, false)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(status); + expect(result.ok).toBe(false); + // Must be called exactly once - no retries for permanent errors + expect(mockSendWebhook).toHaveBeenCalledTimes(1); + }, + ); + }); + + // ── Retryable server errors (5xx) ──────────────────────────────────────── + + describe('retryable server errors (5xx)', () => { + it.each([429, 500, 502, 503, 504])( + 'retries HTTP %i up to the maximum attempts', + async (status) => { + mockSendWebhook.mockResolvedValue(makeResponse(status, false)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(status); + expect(result.ok).toBe(false); + // Should attempt: initial + 2 retries = 3 total + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }, + ); + + it('succeeds after retrying a 503 error once', async () => { + mockSendWebhook + .mockResolvedValueOnce(makeResponse(503, false)) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(200); + expect(result.ok).toBe(true); + // First attempt fails, second succeeds + expect(mockSendWebhook).toHaveBeenCalledTimes(2); + }); + + it('succeeds on the final retry attempt after multiple 500 errors', async () => { + mockSendWebhook + .mockResolvedValueOnce(makeResponse(500, false)) + .mockResolvedValueOnce(makeResponse(500, false)) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(200); + expect(result.ok).toBe(true); + // All 3 attempts used: 2 failures + 1 success + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + + it('returns the final failed response after exhausting all retry attempts', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(503, false)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(503); + expect(result.ok).toBe(false); + // Maximum attempts: 1 initial + 2 retries = 3 + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + }); + + // ── Network errors (retryable) ─────────────────────────────────────────── + + describe('network/connection errors are retried', () => { + it('retries network errors up to the maximum attempts', async () => { + mockSendWebhook.mockRejectedValue(makeNetworkError()); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('ECONNREFUSED'); + + // Should retry: 1 initial + 2 retries = 3 total + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + + it('succeeds after a network error is resolved on retry', async () => { + mockSendWebhook + .mockRejectedValueOnce(makeNetworkError()) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(200); + expect(result.ok).toBe(true); + // First attempt fails with error, second succeeds + expect(mockSendWebhook).toHaveBeenCalledTimes(2); + }); + + it('retries timeout errors (AbortError)', async () => { + mockSendWebhook.mockRejectedValue(makeTimeoutError()); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow('The operation was aborted'); + + // Should retry: 1 initial + 2 retries = 3 total + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + + it('succeeds after a timeout is resolved on retry', async () => { + mockSendWebhook + .mockRejectedValueOnce(makeTimeoutError()) + .mockRejectedValueOnce(makeTimeoutError()) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(200); + expect(result.ok).toBe(true); + // 2 timeouts, then success on 3rd attempt + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + }); + + // ── Retry delay ────────────────────────────────────────────────────────── + + describe('retry delay between attempts', () => { + it('waits 1000ms between retry attempts', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(503, false)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + + // First attempt happens immediately + await Promise.resolve(); + await Promise.resolve(); + expect(mockSendWebhook).toHaveBeenCalledTimes(1); + + // Advance timer by 1000ms to trigger first retry + await jest.advanceTimersByTimeAsync(1000); + expect(mockSendWebhook).toHaveBeenCalledTimes(2); + + // Advance timer by another 1000ms to trigger second retry + await jest.advanceTimersByTimeAsync(1000); + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + + await promise; + }); + + it('does not delay before the initial attempt', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + + // Initial attempt happens immediately + await Promise.resolve(); + expect(mockSendWebhook).toHaveBeenCalledTimes(1); + + await jest.runAllTimersAsync(); + const result = await promise; + expect(result.ok).toBe(true); + }); + + it('does not delay after the final attempt', async () => { + mockSendWebhook + .mockResolvedValueOnce(makeResponse(503, false)) + .mockResolvedValueOnce(makeResponse(503, false)) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + + // Initial attempt happens immediately + await Promise.resolve(); + await Promise.resolve(); + expect(mockSendWebhook).toHaveBeenCalledTimes(1); + + // First retry after delay + await jest.advanceTimersByTimeAsync(1000); + expect(mockSendWebhook).toHaveBeenCalledTimes(2); + + // Second retry after delay (final attempt) + await jest.advanceTimersByTimeAsync(1000); + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + + // Should have no more pending timers + const result = await promise; + expect(result.ok).toBe(true); + }); + }); + + // ── Bounded attempts ───────────────────────────────────────────────────── + + describe('bounded retry attempts', () => { + it('enforces maximum of 3 total attempts (1 initial + 2 retries)', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(500, false)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + await promise; + + // Must not exceed 3 attempts + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + + it('does not create infinite retry loops', async () => { + mockSendWebhook.mockRejectedValue(makeNetworkError()); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + jest.runAllTimersAsync(); + + await expect(promise).rejects.toThrow(); + + // Must stop after maximum attempts + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + expect(mockSendWebhook.mock.calls.length).toBeLessThanOrEqual(3); + }); + }); + + // ── Mixed scenarios ────────────────────────────────────────────────────── + + describe('mixed failure scenarios', () => { + it('retries network error then 503, then succeeds', async () => { + mockSendWebhook + .mockRejectedValueOnce(makeNetworkError()) + .mockResolvedValueOnce(makeResponse(503, false)) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.ok).toBe(true); + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + + it('retries 502, then 429, then fails permanently', async () => { + mockSendWebhook + .mockResolvedValueOnce(makeResponse(502, false)) + .mockResolvedValueOnce(makeResponse(429, false)) + .mockResolvedValueOnce(makeResponse(429, false)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD); + await jest.runAllTimersAsync(); + const result = await promise; + + expect(result.status).toBe(429); + expect(result.ok).toBe(false); + expect(mockSendWebhook).toHaveBeenCalledTimes(3); + }); + }); + + // ── Options passthrough ────────────────────────────────────────────────── + + describe('options passthrough', () => { + it('passes timeout options to sendWebhook', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD, { timeoutMs: 3000 }); + await jest.runAllTimersAsync(); + await promise; + + expect(mockSendWebhook).toHaveBeenCalledWith( + TARGET_URL, + PAYLOAD, + expect.objectContaining({ timeoutMs: 3000 }), + ); + }); + + it('passes custom headers to sendWebhook', async () => { + mockSendWebhook.mockResolvedValue(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD, { + headers: { 'X-Custom': 'value' }, + }); + await jest.runAllTimersAsync(); + await promise; + + expect(mockSendWebhook).toHaveBeenCalledWith( + TARGET_URL, + PAYLOAD, + expect.objectContaining({ headers: { 'X-Custom': 'value' } }), + ); + }); + + it('retries with the same options on each attempt', async () => { + mockSendWebhook + .mockResolvedValueOnce(makeResponse(503, false)) + .mockResolvedValueOnce(makeResponse(200)); + + const promise = sendWebhookWithRetry(TARGET_URL, PAYLOAD, { + timeoutMs: 2500, + headers: { 'X-Retry': 'test' }, + }); + await jest.runAllTimersAsync(); + await promise; + + expect(mockSendWebhook).toHaveBeenNthCalledWith( + 1, + TARGET_URL, + PAYLOAD, + expect.objectContaining({ timeoutMs: 2500, headers: { 'X-Retry': 'test' } }), + ); + expect(mockSendWebhook).toHaveBeenNthCalledWith( + 2, + TARGET_URL, + PAYLOAD, + expect.objectContaining({ timeoutMs: 2500, headers: { 'X-Retry': 'test' } }), + ); + }); + }); + + // ── Failure classification ──────────────────────────────────────────────── + + describe('classifyWebhookFailure', () => { + it('classifies an AbortError as a timeout', () => { + expect(classifyWebhookFailure(undefined, makeTimeoutError())).toBe('timeout'); + }); + + it('classifies a TimeoutError as a timeout', () => { + const err = new Error('request timed out'); + err.name = 'TimeoutError'; + + expect(classifyWebhookFailure(undefined, err)).toBe('timeout'); + }); + + it('classifies a generic error as a network failure (not a timeout)', () => { + expect(classifyWebhookFailure(undefined, makeNetworkError())).toBe('network'); + }); + + it('does not treat a message mentioning "timeout" as a timeout', () => { + // Only the error name marks an abort; a generic message must not be + // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + expect(classifyWebhookFailure(undefined, new Error('timeout'))).toBe('network'); + }); + + it('classifies retryable HTTP statuses', () => { + for (const status of [429, 500, 502, 503, 504]) { + expect(classifyWebhookFailure(makeResponse(status, false))).toBe('http_retryable'); + } + }); + + it('classifies permanent client errors', () => { + for (const status of [400, 401, 403, 404, 422]) { + expect(classifyWebhookFailure(makeResponse(status, false))).toBe('http_permanent'); + } + }); + + it('returns null for a successful response', () => { + expect(classifyWebhookFailure(makeResponse(200))).toBeNull(); + }); + }); +}); diff --git a/_t2.txt b/_t2.txt new file mode 100644 index 00000000..7e46958a --- /dev/null +++ b/_t2.txt @@ -0,0 +1,160 @@ +/** + * Bounded retry helper for transient API failures. + * + * Wraps the sendWebhook function with automatic retry logic for + * transient failures such as: + * - Network/connection errors + * - Timeout (AbortError) + * - HTTP 429 (Too Many Requests) + * - HTTP 500, 502, 503, 504 (Server errors) + * + * Permanent client errors (400, 401, 403, 404, 422) are NOT retried. + * + * Retry attempts are bounded by MAX_RETRY_ATTEMPTS to prevent infinite loops. + * A delay is added between attempts to reduce load on failing services. + */ + +import { sendWebhook, WebhookSendOptions, WebhookFailureReason, isWebhookTimeoutError } from './webhook-sender'; + +/** Maximum number of retry attempts (not counting the initial attempt). */ +const MAX_RETRY_ATTEMPTS = 2; + +/** Delay in milliseconds between retry attempts. */ +const RETRY_DELAY_MS = 1000; + +/** + * HTTP status codes that are considered retryable (transient failures). + */ +const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]); + +/** + * HTTP status codes that are permanent client errors (not retryable). + */ +const PERMANENT_CLIENT_ERRORS = new Set([400, 401, 403, 404, 422]); + +/** + * Classify a webhook attempt as a specific failure reason, or `null` when it + * succeeded / did not fail. + * + * A request timeout is reported as its own `'timeout'` reason rather than + * being folded into a generic network error, so retry logic and observability + * can treat (and count) the two separately. + * + * @param response - The HTTP response, if one was received + * @param error - The error thrown, if the request failed before/without a response + * @returns the failure reason, or null when the attempt did not fail + */ +export function classifyWebhookFailure( + response?: Response, + error?: unknown +): WebhookFailureReason | null { + // A thrown error means no HTTP response was available. Timeouts are their + // own reason; everything else is a network-level failure. + if (error) { + return isWebhookTimeoutError(error) ? 'timeout' : 'network'; + } + + if (!response || response.ok) { + return null; + } + + // Permanent client errors are not worth retrying. + if (PERMANENT_CLIENT_ERRORS.has(response.status)) { + return 'http_permanent'; + } + + // Explicit retryable status codes, plus any other 5xx. + if (RETRYABLE_STATUS_CODES.has(response.status) || response.status >= 500) { + return 'http_retryable'; + } + + // Other status codes (e.g. redirects, unlisted 4xx) are non-retryable. + return 'http_permanent'; +} + +/** + * Determines if an error or response should trigger a retry. + * + * @param response - The HTTP response, if available + * @param error - The error thrown, if any + * @returns true if the failure is retryable + */ +function isRetryable(response?: Response, error?: unknown): boolean { + const reason = classifyWebhookFailure(response, error); + return reason === 'timeout' || reason === 'network' || reason === 'http_retryable'; +} + +/** + * Delay execution for the specified number of milliseconds. + * + * @param ms - Milliseconds to delay + */ +async function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Send a webhook with bounded retry logic for transient failures. + * + * Makes an initial attempt, then retries up to MAX_RETRY_ATTEMPTS times + * if the failure is retryable. Adds a delay between attempts. + * + * @param url - Target webhook URL + * @param payload - JSON-serializable payload + * @param opts - Webhook send options (timeout, headers) + * @returns Response object or throws the final error + * @throws The last error encountered after all retry attempts are exhausted + */ +export async function sendWebhookWithRetry( + url: string, + payload: any, + opts: WebhookSendOptions = {}, +): Promise { + let lastError: unknown; + let lastResponse: Response | undefined; + + // Initial attempt + retry attempts + const maxAttempts = 1 + MAX_RETRY_ATTEMPTS; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + const response = await sendWebhook(url, payload, opts); + + // Success case + if (response.ok) { + return response; + } + + // Non-retryable failure (permanent client error) + if (!isRetryable(response, undefined)) { + return response; + } + + // Retryable failure - store response and retry if attempts remain + lastResponse = response; + + if (attempt < maxAttempts - 1) { + await delay(RETRY_DELAY_MS); + } + } catch (error) { + lastError = error; + + // If this was the last attempt, throw the error + if (attempt === maxAttempts - 1) { + throw error; + } + + // Otherwise, delay and retry + await delay(RETRY_DELAY_MS); + } + } + + // If we got here, we have a failed response (not an exception) + // Return the last response + if (lastResponse) { + return lastResponse; + } + + // This should not happen, but handle it gracefully + throw lastError ?? new Error('All retry attempts failed'); +} diff --git a/_tc.txt b/_tc.txt new file mode 100644 index 00000000..cfbf99b4 --- /dev/null +++ b/_tc.txt @@ -0,0 +1,4 @@ + +> listener@1.0.0 typecheck +> node ./node_modules/typescript/bin/tsc --noEmit + diff --git a/_tgt1.txt b/_tgt1.txt new file mode 100644 index 00000000..cad2db66 --- /dev/null +++ b/_tgt1.txt @@ -0,0 +1,84 @@ +node : FAIL src/__tests__/notification-flow-e2e.test.ts +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=1 src/_ ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/__test...low-e2e.test.ts:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\__tests__\notification-flow-e2e.test.ts:302 + it(, async) { } + ^ + + SyntaxError: Unexpected token ',' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/webhook-verifier.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\services\webhook-verifier.test.ts:458 + expect(outcome) { }, : .authenticated + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +Test Suites: 2 failed, 2 total +Tests: 0 total +Snapshots: 0 total +Time: 9.289 s +Ran all test suites matching src/__tests__/notification-flow-e2e.test.ts|src/services/webhook-verifier.test.ts. diff --git a/_tgt2.txt b/_tgt2.txt new file mode 100644 index 00000000..ae1ebeef --- /dev/null +++ b/_tgt2.txt @@ -0,0 +1,513 @@ +node : ts-jest[ts-compiler] (WARN) src/__tests__/notification-flow-e2e.test.ts:171:11 - +error TS2304: Cannot find name 'scheduler'. +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=1 src/_ ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (ts-jest[ts-comp...me 'scheduler'.:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + +171 await scheduler.stop(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:247:13 - error TS2304: Cannot +find name 'scheduler'. + +247 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:341:47 - error TS2349: This +expression is not callable. + Type '{ payload: { message: string; }; notificationType: NotificationType; targetRecipient: string; executeAt: Date; +it(: any, async: any): any; }' has no call signatures. + +341 const id = await api.scheduleNotification({ +   ~ +342 payload: { message: 'deliver me' }, +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... +345 executeAt: new Date('2026-06-24T12:00:02.000Z'), +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +346 it('should log execution attempts for audit trail', async () => { +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:346:8 - error TS7006: +Parameter '(Missing)' implicitly has an 'any' type. + +346 it('should log execution attempts for audit trail', async () => { +    +src/__tests__/notification-flow-e2e.test.ts:346:57 - error TS7006: +Parameter 'async' implicitly has an 'any' type. + +346 it('should log execution attempts for audit trail', async () => { +   ~~~~~ +src/__tests__/notification-flow-e2e.test.ts:368:11 - error TS2454: +Variable 'scheduler' is used before being assigned. + +368 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:371:23 - error TS2454: +Variable 'repository' is used before being assigned. + +371 const row = await repository.getById(id); +   ~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:371:42 - error TS2304: Cannot +find name 'id'. + +371 const row = await repository.getById(id); +   ~~ +src/__tests__/notification-flow-e2e.test.ts:373:12 - error TS2454: +Variable 'sendMock' is used before being assigned. + +373 expect(sendMock).toHaveBeenCalledTimes(1); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:389:11 - error TS2304: Cannot +find name 'scheduler'. + +389 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:394:12 - error TS2304: Cannot +find name 'sendMock'. + +394 expect(sendMock).not.toHaveBeenCalled(); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:405:11 - error TS2304: Cannot +find name 'scheduler'. + +405 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:416:12 - error TS2304: Cannot +find name 'sendMock'. + +416 expect(sendMock).toHaveBeenCalledTimes(1); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:427:11 - error TS2304: Cannot +find name 'scheduler'. + +427 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:455:11 - error TS2304: Cannot +find name 'scheduler'. + +455 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:461:12 - error TS2304: Cannot +find name 'sendMock'. + +461 expect(sendMock).toHaveBeenCalledTimes(3); +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:476:11 - error TS2304: Cannot +find name 'scheduler'. + +476 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:479:12 - error TS2304: Cannot +find name 'sendMock'. + +479 expect(sendMock).toHaveBeenCalledWith( +   ~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:501:53 - error TS2304: Cannot +find name 'idempotencyKey'. + +501 [new Date(Date.now() - 1000).toISOString(), idempotencyKey] +   ~~~~~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:507:7 - error TS2304: Cannot +find name 'stats'. + +507 stats = await idempotencyRepo.getStats(); +   ~~~~~ +src/__tests__/notification-flow-e2e.test.ts:508:14 - error TS2304: Cannot +find name 'stats'. + +508 expect(stats.expired).toBeGreaterThanOrEqual(0); +   ~~~~~ +src/__tests__/notification-flow-e2e.test.ts:907:13 - error TS2304: Cannot +find name 'scheduler'. + +907 await scheduler.start(); +   ~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:346:8 - error TS1003: +Identifier expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/__tests__/notification-flow-e2e.test.ts:346:55 - error TS1138: +Parameter declaration expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~ +src/__tests__/notification-flow-e2e.test.ts:346:63 - error TS1005: ',' +expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~ +src/__tests__/notification-flow-e2e.test.ts:346:66 - error TS1005: ',' +expected. + +346 it('should log execution attempts for audit trail', async () => { +   ~~ +src/__tests__/notification-flow-e2e.test.ts:366:6 - error TS1128: +Declaration or statement expected. + +366 }); +   ~ +src/__tests__/notification-flow-e2e.test.ts:985:1 - error TS1005: '}' +expected. + +985 +   + + src/__tests__/notification-flow-e2e.test.ts:857:63 + 857 describe('5. Backpressure and idempotency integration', () => { +    ~ + The parser expected to find a '}' to match the '{' token here. +FAIL src/__tests__/notification-flow-e2e.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\__tests__\notification-flow-e2e.test.ts:302 + it(, async) { } + ^ + + SyntaxError: Unexpected token ',' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +ts-jest[ts-compiler] (WARN) src/services/webhook-verifier.test.ts:537:11 - +error TS7022: 'outcome' implicitly has type 'any' because it does not have a type annotation and is +referenced directly or indirectly in its own initializer. + +537 const outcome = verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:539:5 - error TS2353: Object +literal may only specify known properties, and 'const' does not exist in type 'WebhookVerificationContext'. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~~~ +src/services/webhook-verifier.test.ts:539:11 - error TS2588: Cannot +assign to 'sig' because it is a constant. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~ +src/services/webhook-verifier.test.ts:540:11 - error TS2448: Block-scoped +variable 'outcome' used before its declaration. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ + + src/services/webhook-verifier.test.ts:537:11 + 537 const outcome = verifyWebhookRequest({ +    ~~~~~~~ + 'outcome' is declared here. +src/services/webhook-verifier.test.ts:540:11 - error TS2588: Cannot +assign to 'outcome' because it is a constant. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:550:12 - error TS7006: Parameter +'outcome' implicitly has an 'any' type. + +550 expect(outcome.authenticated).toBe(false); +   ~~~~~~~ +src/services/webhook-verifier.test.ts:550:35 - error TS2339: Property +'toBe' does not exist on type 'Promise'. + +550 expect(outcome.authenticated).toBe(false); +   ~~~~ +src/services/webhook-verifier.test.ts:604:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +604 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:604:20 + 604 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:605:20 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +605 expect(outcome.statusCode).toBe(401); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:605:20 + 605 expect(outcome.statusCode).toBe(401); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:606:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +606 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:606:20 + 606 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:616:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +616 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:616:20 + 616 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:631:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +631 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:631:20 + 631 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:642:21 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +642 expect(replay.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:642:21 + 642 expect(replay.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:643:21 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +643 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:643:21 + 643 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:660:18 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +660 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:660:18 + 660 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:671:19 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +671 expect(second.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:671:19 + 671 expect(second.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:672:19 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +672 expect(second.statusCode).toBe(409); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:672:19 + 672 expect(second.statusCode).toBe(409); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:673:19 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +673 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:673:19 + 673 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:698:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +698 expect(a.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:698:14 + 698 expect(a.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:699:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +699 expect(b.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:699:14 + 699 expect(b.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:712:16 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +712 expect(bad.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:712:16 + 712 expect(bad.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:721:17 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +721 expect(good.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:721:17 + 721 expect(good.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:732:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +732 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:732:20 + 732 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:733:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +733 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:733:20 + 733 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:743:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +743 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:743:20 + 743 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:744:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +744 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:744:20 + 744 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:759:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +759 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:759:20 + 759 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:773:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +773 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:773:20 + 773 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:539:11 - error TS1005: ':' expected. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~ +src/services/webhook-verifier.test.ts:539:68 - error TS1005: ',' expected. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~ +src/services/webhook-verifier.test.ts:540:11 - error TS1005: ':' expected. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:549:7 - error TS1005: ',' expected. + +549 }); +   ~ +src/services/webhook-verifier.test.ts:550:19 - error TS1005: ',' expected. + +550 expect(outcome.authenticated).toBe(false); +   ~ +src/services/webhook-verifier.test.ts:550:33 - error TS1005: ',' expected. + +550 expect(outcome.authenticated).toBe(false); +   ~ +FAIL src/services/webhook-verifier.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\services\webhook-verifier.test.ts:458 + expect(outcome) { }, : .authenticated + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +Test Suites: 2 failed, 2 total +Tests: 0 total +Snapshots: 0 total +Time: 17.391 s +Ran all test suites matching src/__tests__/notification-flow-e2e.test.ts|src/services/webhook-verifier.test.ts. diff --git a/_tscheck.mjs b/_tscheck.mjs new file mode 100644 index 00000000..6b64d345 --- /dev/null +++ b/_tscheck.mjs @@ -0,0 +1,38 @@ +import fs from 'fs'; +import path from 'path'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const ts = require(path.resolve('listener/node_modules/typescript/lib/typescript.js')); + +const file = process.argv[2]; +const src = fs.readFileSync(file, 'utf8'); +const sf = ts.createSourceFile(file, src, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + +const lineOf = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1; +const colOf = (pos) => sf.getLineAndCharacterOfPosition(pos).character + 1; + +const target = Number(process.argv[3]); // line number of interest +let found = null; + +function walk(node) { + const s = lineOf(node.getStart(sf)); + const e = lineOf(node.getEnd()); + if (s <= target && e >= target) { + found = node; + } + node.forEachChild(walk); +} + +walk(sf); +console.log(`Innermost node containing line ${target}: ${ts.SyntaxKind[found.kind]} ${lineOf(found.getStart(sf))}:${colOf(found.getStart(sf))} - ${lineOf(found.getEnd())}:${colOf(found.getEnd())}`); +console.log('text:'); +console.log(found.getText(sf).slice(0, 400)); + +let parent = found.parent; +let depth = 0; +while (parent && depth < 8) { + console.log(` ^ parent ${depth}: ${ts.SyntaxKind[parent.kind]} ${lineOf(parent.getStart(sf))}:${colOf(parent.getStart(sf))} - ${lineOf(parent.getEnd())}:${colOf(parent.getEnd())}`); + parent = parent.parent; + depth++; +} diff --git a/_typecheck-errors.txt b/_typecheck-errors.txt new file mode 100644 index 00000000..c0631c64 --- /dev/null +++ b/_typecheck-errors.txt @@ -0,0 +1,113 @@ + +> listener@1.0.0 typecheck +> node ./node_modules/typescript/bin/tsc --noEmit + +src/api/events-server.ts(2,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/api/events-server.ts(12,10): error TS2300: Duplicate identifier 'TemplateService'. +src/api/events-server.ts(13,10): error TS2300: Duplicate identifier 'handleTemplateRoutes'. +src/api/events-server.ts(14,10): error TS2300: Duplicate identifier 'sendOk'. +src/api/events-server.ts(14,18): error TS2300: Duplicate identifier 'sendErr'. +src/api/events-server.ts(14,27): error TS2300: Duplicate identifier 'sendJson'. +src/api/events-server.ts(14,37): error TS2300: Duplicate identifier 'ErrorCode'. +src/api/events-server.ts(18,10): error TS2300: Duplicate identifier 'TemplateService'. +src/api/events-server.ts(19,10): error TS2300: Duplicate identifier 'handleTemplateRoutes'. +src/api/events-server.ts(20,10): error TS2300: Duplicate identifier 'sendOk'. +src/api/events-server.ts(20,18): error TS2300: Duplicate identifier 'sendErr'. +src/api/events-server.ts(20,27): error TS2300: Duplicate identifier 'sendJson'. +src/api/events-server.ts(20,37): error TS2300: Duplicate identifier 'ErrorCode'. +src/api/events-server.ts(27,3): error TS2300: Duplicate identifier 'IdempotencyKeyService'. +src/api/events-server.ts(28,3): error TS2300: Duplicate identifier 'IdempotencyKeyReuseError'. +src/api/events-server.ts(31,10): error TS2300: Duplicate identifier 'IdempotencyKeyService'. +src/api/events-server.ts(31,33): error TS2300: Duplicate identifier 'IdempotencyKeyReuseError'. +src/api/events-server.ts(121,31): error TS2304: Cannot find name 'DeliveryReceiptRepository'. +src/api/events-server.ts(138,23): error TS2304: Cannot find name 'DataExportService'. +src/api/events-server.ts(536,20): error TS2304: Cannot find name 'authenticateApiKey'. +src/api/events-server.ts(546,25): error TS2304: Cannot find name 'API_KEY_AUTH_MESSAGES'. +src/api/events-server.ts(632,17): error TS2304: Cannot find name 'parseLedgerRangeParams'. +src/api/events-server.ts(634,28): error TS2304: Cannot find name 'ValidationError'. +src/api/events-server.ts(635,76): error TS2304: Cannot find name 'validationErrorBody'. +src/api/events-server.ts(835,17): error TS2451: Cannot redeclare block-scoped variable 'secrets'. +src/api/events-server.ts(836,17): error TS2451: Cannot redeclare block-scoped variable 'maxAgeSeconds'. +src/api/events-server.ts(837,15): error TS2451: Cannot redeclare block-scoped variable 'secrets'. +src/api/events-server.ts(838,15): error TS2451: Cannot redeclare block-scoped variable 'maxAgeSeconds'. +src/api/events-server.ts(846,15): error TS2451: Cannot redeclare block-scoped variable 'auth'. +src/api/events-server.ts(859,17): error TS2451: Cannot redeclare block-scoped variable 'auth'. +src/api/events-server.ts(1061,46): error TS2304: Cannot find name 'DataExportService'. +src/api/events-server.ts(1063,30): error TS2304: Cannot find name 'validatePayload'. +src/api/events-server.ts(1063,58): error TS2304: Cannot find name 'Schemas'. +src/api/events-server.ts(1547,23): error TS2304: Cannot find name 'DeliveryReceiptStatus'. +src/api/events-server.ts(1548,56): error TS2304: Cannot find name 'DeliveryReceiptStatus'. +src/api/events-server.ts(1554,68): error TS2304: Cannot find name 'DeliveryReceiptStatus'. +src/api/events-server.ts(1555,16): error TS7006: Parameter 'receipts' implicitly has an 'any' type. +src/api/events-server.ts(1556,17): error TS7006: Parameter 'error' implicitly has an 'any' type. +src/api/request-duration-logger.ts(55,34): error TS2345: Argument of type 'RequestDurationLogData' is not assignable to parameter of type 'Record'. + Index signature for type 'string' is missing in type 'RequestDurationLogData'. +src/config.ts(263,43): error TS2304: Cannot find name 'RetryPolicyOptions'. +src/config.ts(280,5): error TS1117: An object literal cannot have multiple properties with the same name. +src/config.ts(297,35): error TS2304: Cannot find name 'RetryPolicyOptions'. +src/config.ts(304,30): error TS2304: Cannot find name 'RetryFailureType'. +src/config.ts(307,7): error TS2552: Cannot find name 'parseRetryableFailureTypes'. Did you mean 'retryableFailureTypes'? +src/config.ts(308,11): error TS2552: Cannot find name 'DEFAULT_RETRYABLE_FAILURE_TYPES'. Did you mean 'retryableFailureTypes'? +src/config.ts(376,36): error TS2304: Cannot find name 'RpcRateLimitConfig'. +src/config.ts(385,57): error TS2304: Cannot find name 'RpcFallbackConfig'. +src/config.ts(395,38): error TS2304: Cannot find name 'CircuitBreakerConfig'. +src/config.ts(492,5): error TS2353: Object literal may only specify known properties, and 'circuitBreaker' does not exist in type 'Config'. +src/config.ts(813,20): error TS2304: Cannot find name 'RETRY_FAILURE_TYPES'. +src/config.ts(818,62): error TS2304: Cannot find name 'RETRY_FAILURE_TYPES'. +src/examples/schedule-notification-example.ts(120,39): error TS2345: Argument of type 'NotificationStats' is not assignable to parameter of type 'LogContext'. + Index signature for type 'string' is missing in type 'NotificationStats'. +src/index.ts(25,10): error TS2300: Duplicate identifier 'EventDeduplicationService'. +src/index.ts(35,10): error TS2300: Duplicate identifier 'EventDeduplicationService'. +src/scripts/replay-event.ts(18,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/discord-notification.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/discord-notification.ts(406,9): error TS1117: An object literal cannot have multiple properties with the same name. +src/services/discord-notification.ts(430,9): error TS1117: An object literal cannot have multiple properties with the same name. +src/services/event-normalization.ts(11,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/event-processing-queue.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/event-subscriber.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/historical-backfill-service.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/historical-backfill-service.ts(134,13): error TS2339: Property 'startLedger' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(141,37): error TS2339: Property 'maxPages' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(142,49): error TS2339: Property 'maxEventsPerContract' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(185,29): error TS2339: Property 'endLedger' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(186,34): error TS2339: Property 'endLedger' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(206,42): error TS2339: Property 'maxPages' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(207,43): error TS2339: Property 'maxEventsPerContract' does not exist on type 'BackfillConfig'. +src/services/historical-backfill-service.ts(258,38): error TS7006: Parameter 'ev' implicitly has an 'any' type. +src/services/indexing-reconciliation-engine.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/mock-notification-provider.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/mock-notification-transport.ts(9,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/notification-expiration.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/notification-retry-queue.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/notification-retry-queue.ts(22,13): error TS2304: Cannot find name 'PartialRetryBackoffConfig'. +src/services/notification-retry-queue.ts(61,38): error TS2304: Cannot find name 'RetryBackoffConfig'. +src/services/notification-retry-queue.ts(83,20): error TS2304: Cannot find name 'resolveRetryBackoffConfig'. +src/services/notification-retry-queue.ts(83,46): error TS18048: 'options' is possibly 'undefined'. +src/services/notification-retry-queue.ts(84,30): error TS18048: 'options' is possibly 'undefined'. +src/services/notification-retry-queue.ts(85,28): error TS18048: 'options' is possibly 'undefined'. +src/services/notification-retry-queue.ts(115,21): error TS2304: Cannot find name 'calculateBackoffDelay'. +src/services/notification-retry-queue.ts(239,70): error TS2339: Property 'maxRetries' does not exist on type 'NotificationRetryQueue'. +src/services/notification-retry-queue.ts(284,21): error TS2304: Cannot find name 'calculateBackoffDelay'. +src/services/providers/discord-provider.ts(71,37): error TS2304: Cannot find name 'errorText'. +src/services/providers/discord-provider.ts(76,73): error TS2304: Cannot find name 'errorText'. +src/services/retry-scheduler.ts(65,14): error TS2741: Property 'backoff' is missing in type '{ enabled: true; pollIntervalMs: number; lockTimeoutMs: number; batchSize: number; baseDelayMs: number; multiplier: number; maxDelayMs: number; jitter: true; webhookTimeoutMs: number; }' but required in type 'Readonly & { backoff: RetryBackoffConfig; }>'. +src/services/retry-scheduler.ts(66,21): error TS2304: Cannot find name 'RetryBackoffConfig'. +src/services/stellar-rpc-manager.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/stellar-rpc-manager.ts(3,10): error TS2305: Module '"../types"' has no exported member 'RpcEndpointStatus'. +src/services/stellar-rpc-manager.ts(3,29): error TS2305: Module '"../types"' has no exported member 'RpcFallbackConfig'. +src/services/subscription-event-emitter.ts(25,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/services/webhook-delivery-service.ts(151,13): error TS2451: Cannot redeclare block-scoped variable 'isTimeout'. +src/services/webhook-delivery-service.ts(153,13): error TS2451: Cannot redeclare block-scoped variable 'isTimeout'. +src/services/webhook-retry-helper.ts(73,7): error TS2304: Cannot find name 'PERMANENT_CLIENT_ERRORS'. +src/services/webhook-retry-helper.ts(78,7): error TS2304: Cannot find name 'RETRYABLE_STATUS_CODES'. +src/services/webhook-retry-helper.ts(142,45): error TS2554: Expected 0-2 arguments, but got 3. +src/test-utils/notification-fixture-builder.ts(34,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/test-utils/notification-fixture-builder.ts(35,21): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/types/index.ts(3,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/types/index.ts(39,13): error TS2304: Cannot find name 'PartialRetryBackoffConfig'. +src/types/index.ts(66,17): error TS2304: Cannot find name 'RpcFallbackConfig'. +src/types/registry-event-input.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/utils/config-snapshot.ts(8,10): error TS2305: Module '"./redact"' has no exported member 'redactSensitiveData'. +src/utils/event-utils.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. +src/utils/json-logger-formatter.ts(9,10): error TS2305: Module '"./redact"' has no exported member 'redactSensitiveData'. +src/utils/scval-format.ts(1,29): error TS2307: Cannot find module '@stellar/stellar-sdk' or its corresponding type declarations. diff --git a/_typecheck-main.txt b/_typecheck-main.txt new file mode 100644 index 00000000..c1bfe228 --- /dev/null +++ b/_typecheck-main.txt @@ -0,0 +1,42 @@ + +> listener@1.0.0 typecheck +> node ./node_modules/typescript/bin/tsc --noEmit + +src/api/events-server.ts(2124,4): error TS1128: Declaration or statement expected. +src/middleware/security-headers.ts(21,1): error TS1359: Identifier expected. 'const' is a reserved word that cannot be used here. +src/middleware/security-headers.ts(21,7): error TS1005: ',' expected. +src/middleware/security-headers.ts(22,77): error TS1005: ',' expected. +src/middleware/security-headers.ts(24,8): error TS1359: Identifier expected. 'function' is a reserved word that cannot be used here. +src/middleware/security-headers.ts(63,1): error TS1005: '}' expected. +src/schema/compatibility-check.ts(275,1): error TS1005: '}' expected. +src/services/webhook-sender.ts(28,2): error TS1109: Expression expected. +src/services/webhook-sender.ts(28,19): error TS1005: ';' expected. +src/services/webhook-sender.ts(28,22): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(28,25): error TS1435: Unknown keyword or identifier. Did you mean 'out bound'? +src/services/webhook-sender.ts(28,34): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(29,2): error TS1003: Identifier expected. +src/services/webhook-sender.ts(30,2): error TS1109: Expression expected. +src/services/webhook-sender.ts(30,9): error TS1005: ';' expected. +src/services/webhook-sender.ts(30,22): error TS1435: Unknown keyword or identifier. Did you mean 'case'? +src/services/webhook-sender.ts(30,29): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(30,32): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(30,37): error TS1435: Unknown keyword or identifier. Did you mean 'try'? +src/services/webhook-sender.ts(30,43): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(30,53): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(30,57): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(30,71): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,6): error TS1005: ';' expected. +src/services/webhook-sender.ts(31,14): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,22): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,28): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,33): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,35): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,43): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,51): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,57): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(31,60): error TS1434: Unexpected keyword or identifier. +src/services/webhook-sender.ts(32,12): error TS1005: ';' expected. +src/services/webhook-sender.ts(32,36): error TS1005: ';' expected. +src/services/webhook-sender.ts(33,2): error TS1003: Identifier expected. +src/services/webhook-sender.ts(33,3): error TS1161: Unterminated regular expression literal. +src/services/webhook-verifier.ts(469,1): error TS1005: '}' expected. diff --git a/_v1e0.txt b/_v1e0.txt new file mode 100644 index 00000000..f7354eb1 --- /dev/null +++ b/_v1e0.txt @@ -0,0 +1,968 @@ +/** + * End-to-end tests for the complete notification flow. + * Issue: Core-Foundry/Notify-Chain#247 + * + * Covers every phase the issue requires: + * 1. Notification creation — input validation, scheduling, idempotency + * 2. Delivery workflows — scheduler poll → send → status update → audit log + * 3. Retry scenarios — exponential backoff, distributed lock, next_retry_at + * 4. Failure handling — max-retry exhaustion, permanent failure, stale locks + * + * The only thing mocked is the outbound network (fetch / Discord webhook). + * All other collaborators — Database, Repository, Scheduler, RetryScheduler, + * NotificationAPI, IdempotencyKeyService — run against a real in-process SQLite + * database so the full SQL path is exercised. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { Database } from '../database/database'; +import { ScheduledNotificationRepository } from '../services/scheduled-notification-repository'; +import { IdempotencyKeyRepository } from '../services/idempotency-key-repository'; +import { IdempotencyKeyService } from '../services/idempotency-key-service'; +import { NotificationAPI } from '../services/notification-api'; +import { NotificationScheduler } from '../services/notification-scheduler'; +import { RetryScheduler, RETRY_SCHEDULER_DEFAULTS, calculateBackoffDelay } from '../services/retry-scheduler'; +import { DiscordNotificationService } from '../services/discord-notification'; +import { BackpressureController } from '../services/backpressure-controller'; +import { BackpressureMonitor } from '../services/backpressure-monitor'; +import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-builder'; +import { resetWorkerManager } from '../services/worker-manager'; +import { NotificationStatus, NotificationType } from '../types/scheduled-notification'; + +jest.mock('../utils/logger', () => ({ + __esModule: true, + default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, +})); +jest.mock('../utils/request-id', () => ({ generateRequestId: () => 'e2e-req-id' })); + +const logger = jest.requireMock('../utils/logger').default; + +// --------------------------------------------------------------------------- +// Shared test database helpers +// --------------------------------------------------------------------------- + +const TEST_DB_PATH = './data/test-notification-flow-e2e.db'; + +async function setupDb(): Promise { + const dbDir = path.dirname(TEST_DB_PATH); + if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true }); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + const db = new Database(TEST_DB_PATH); + await db.initialize(); + return db; +} + +async function clearTables(db: Database): Promise { + await db.run('DELETE FROM notification_execution_log'); + await db.run('DELETE FROM scheduled_notifications'); + await db.run('DELETE FROM idempotency_keys'); + await db.run('DELETE FROM backpressure_events'); +} + +/** Insert a notification directly at PENDING with retry_count > 0 so + * RetryScheduler picks it up (it ignores retry_count === 0). */ +async function insertRetryable( + repo: ScheduledNotificationRepository, + db: Database, + retryCount = 1, + nextRetryAt: Date | null = null, + maxRetries = 3 +): Promise { + const id = await repo.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(maxRetries) + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + await db.run( + `UPDATE scheduled_notifications + SET status = ?, retry_count = ?, next_retry_at = ? + WHERE id = ?`, + [NotificationStatus.PENDING, retryCount, nextRetryAt?.toISOString() ?? null, id] + ); + return id; +} + +const SCHEDULER_CONFIG = { + enabled: true, + pollIntervalMs: 100, + lockTimeoutMs: 30_000, + batchSize: 10, + timingBufferMs: 0, + processorId: 'e2e-processor', +}; + +// =========================================================================== +// 1. NOTIFICATION CREATION +// =========================================================================== + +describe('1. Notification creation', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let idempotencyRepo: IdempotencyKeyRepository; + let idempotencyService: IdempotencyKeyService; + let api: NotificationAPI; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + idempotencyRepo = new IdempotencyKeyRepository(db); + idempotencyService = new IdempotencyKeyService(idempotencyRepo); + api = new NotificationAPI(repository, idempotencyService); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + resetWorkerManager(); + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + await clearTables(db); + }); + + afterEach(() => jest.useRealTimers()); + + it('creates a notification and persists it as PENDING', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const id = await api.scheduleNotification({ + payload: { message: 'hello' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt, + maxRetries: 3, + }); + + expect(id).toBeGreaterThan(0); + const row = await repository.getById(id); + expect(row).toBeTruthy(); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.retryCount).toBe(0); + expect(row!.maxRetries).toBe(3); + }); + + it('rejects a notification with a past executeAt', async () => { + await expect( + api.scheduleNotification({ + payload: { message: 'too late' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T11:59:59.000Z'), // 1 second in the past + }) + ).rejects.toThrow('executeAt must be a future timestamp'); + }); + + it('rejects a notification with a missing targetRecipient', async () => { + await expect( + api.scheduleNotification({ + payload: { message: 'no recipient' }, + notificationType: NotificationType.DISCORD, + targetRecipient: '', + executeAt: new Date('2026-06-24T13:00:00.000Z'), + }) + ).rejects.toThrow('targetRecipient is required'); + }); + afterEach(async () => { + await scheduler.stop(); + }); + + async function waitForSchedulerPolls(ms = 300): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); + } + + function futureExecuteAt(offsetMs = 10000): Date { + return new Date(Date.now() + offsetMs); + } + + describe('Complete notification lifecycle', () => { + it('should create, process, and deliver a notification', async () => { + const executeAt = futureExecuteAt(50); + + it('rejects a notification with an invalid payload', async () => { + await expect( + api.scheduleNotification({ + payload: null as any, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T13:00:00.000Z'), + }) + ).rejects.toThrow('payload must be a valid object'); + }); + + it('schedules a Discord notification via the convenience helper', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const id = await api.scheduleDiscordNotification( + NotificationFixtureBuilder.constants.webhookUrl, + { text: 'convenience' }, + executeAt + ); + expect(id).toBeGreaterThan(0); + const row = await repository.getById(id); + expect(row!.notificationType).toBe(NotificationType.DISCORD); + }); + + it('stores notification metadata when provided', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const id = await api.scheduleNotification({ + payload: { message: 'with meta' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt, + metadata: { source: 'contract', contractAddress: 'CXYZ' }, + }); + + const row = await repository.getById(id); + const meta = JSON.parse(row!.metadata!); + expect(meta.source).toBe('contract'); + expect(meta.contractAddress).toBe('CXYZ'); + }); + + // -- Idempotency -- + + it('returns the same ID for duplicate requests sharing an idempotency key', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const payload = { message: 'idempotent' }; + const key = 'idem-create-1'; + + const id1 = await api.scheduleNotification( + { payload, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + const id2 = await api.scheduleNotification( + { payload, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + + expect(id1).toBe(id2); + const stats = await repository.getStats(); + expect(stats.pending).toBe(1); + }); + await scheduler.start(); + await waitForSchedulerPolls(400); + + it('rejects a duplicate idempotency key used with a different payload', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const key = 'idem-create-2'; + + await api.scheduleNotification( + { payload: { message: 'original' }, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + + await expect( + api.scheduleNotification( + { payload: { message: 'tampered' }, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ) + ).rejects.toThrow('Idempotency key reused with different request body'); + }); + + it('cancels a scheduled notification', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'cancel me' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T13:00:00.000Z'), + }); + + const cancelled = await api.cancelNotification(id); + expect(cancelled).toBe(true); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.CANCELLED); + }); +}); + +// =========================================================================== +// 2. DELIVERY WORKFLOWS +// =========================================================================== + +describe('2. Delivery workflows', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let api: NotificationAPI; + let scheduler: NotificationScheduler; + let sendMock: jest.Mock; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + api = new NotificationAPI(repository); + }); + + afterAll(async () => { + await scheduler?.stop(); + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + await clearTables(db); + + sendMock = jest.fn().mockResolvedValue(true); + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + }); + + afterEach(async () => { + await scheduler.stop(); + jest.useRealTimers(); + }); + + it('processes a due notification and transitions it to COMPLETED', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'deliver me' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:00:02.000Z'), + it('should log execution attempts for audit trail', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'Test notification' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt: futureExecuteAt(50), + }); + + await scheduler.start(); + await waitForSchedulerPolls(400); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id] + ); + + expect(logs).toHaveLength(1); + expect(logs[0].status).toBe('SUCCESS'); + expect(logs[0].execution_attempt).toBe(1); + expect(logs[0].scheduled_notification_id).toBe(id); + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it('does not deliver a notification before its executeAt time', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'not yet' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:10:00.000Z'), // 10 min future + }); + describe('Idempotency handling', () => { + it('should return cached response for duplicate requests with same idempotency key', async () => { + const executeAt = futureExecuteAt(); + const payload = { message: 'Unique message' }; + const idempotencyKey = 'test-idempotency-key-1'; + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(sendMock).not.toHaveBeenCalled(); + }); + + it('delivers once executeAt elapses mid-run', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'wait for it' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:05:00.000Z'), + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(200); + expect((await repository.getById(id))!.status).toBe(NotificationStatus.PENDING); + it('should reject duplicate requests with different payload', async () => { + const executeAt = futureExecuteAt(); + const idempotencyKey = 'test-idempotency-key-2'; + + jest.setSystemTime(new Date('2026-06-24T12:05:01.000Z')); + await jest.advanceTimersByTimeAsync(400); + + expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it('writes a SUCCESS execution log entry after delivery', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'audit this' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:00:02.000Z'), + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + it('should clean up expired idempotency keys', async () => { + const executeAt = futureExecuteAt(); + const idempotencyKey = 'test-idempotency-key-3'; + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id] + ); + expect(logs).toHaveLength(1); + expect(logs[0].status).toBe('SUCCESS'); + expect(logs[0].execution_attempt).toBe(1); + }); + + it('processes a batch of multiple due notifications', async () => { + const executeAt = new Date('2026-06-24T12:00:02.000Z'); + const ids = await Promise.all( + Array.from({ length: 3 }, (_, i) => + api.scheduleNotification({ + payload: { message: `batch-${i}` }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt, + }) + ) + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + for (const id of ids) { + expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); + } + expect(sendMock).toHaveBeenCalledTimes(3); + }); + + it('forwards the event and contract config to the Discord service', async () => { + const event = NotificationFixtureBuilder.aStellarEvent().withId('deliver-evt-1').build(); + const contractConfig = NotificationFixtureBuilder.aContractConfig().build(); + + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withPayload({ event, contractConfig }) + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + expect(sendMock).toHaveBeenCalledWith( + expect.objectContaining({ id: 'deliver-evt-1' }), + expect.objectContaining({ address: contractConfig.address }), + expect.any(String) + ); + expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); + }); +}); + +// =========================================================================== +// 3. RETRY SCENARIOS +// =========================================================================== + +describe('3. Retry scenarios', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + await db.run( + 'UPDATE idempotency_keys SET expires_at = ? WHERE idempotency_key = ?', + [new Date(Date.now() - 1000).toISOString(), idempotencyKey] + ); + + const cleanupCount = await idempotencyService.cleanupExpiredKeys(); + expect(cleanupCount).toBeGreaterThanOrEqual(1); + + stats = await idempotencyRepo.getStats(); + expect(stats.expired).toBeGreaterThanOrEqual(0); + }); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await clearTables(db); + }); + + // -- RetryScheduler (DB-backed) -- + + it('picks up a retryable notification and marks it COMPLETED on success', async () => { + const id = await insertRetryable(repository, db, 1); + const sendMock = jest.fn().mockResolvedValue(true); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: sendMock } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it('writes a SUCCESS execution log on retry delivery', async () => { + const id = await insertRetryable(repository, db, 1); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(true) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id] + ); + expect(logs.length).toBeGreaterThanOrEqual(1); + expect(logs.find((l: any) => l.status === 'SUCCESS')).toBeTruthy(); + }); + + it('schedules next_retry_at with exponential backoff when delivery fails', async () => { + const id = await insertRetryable(repository, db, 1); + const beforeRun = Date.now(); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, baseDelayMs: 1_000, multiplier: 2, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ retry_count: number; next_retry_at: string | null; status: string }>( + 'SELECT retry_count, next_retry_at, status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.retry_count).toBe(2); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.next_retry_at).not.toBeNull(); + + const expectedDelay = calculateBackoffDelay(1, 1_000, 2, RETRY_SCHEDULER_DEFAULTS.maxDelayMs, false); + const actualDelay = new Date(row!.next_retry_at!).getTime() - beforeRun; + expect(actualDelay).toBeGreaterThanOrEqual(expectedDelay * 0.9); + }); + + it('does not process a notification whose next_retry_at is in the future', async () => { + const futureRetry = new Date(Date.now() + 60_000); + await insertRetryable(repository, db, 1, futureRetry); + const sendMock = jest.fn(); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS }, + { sendEventNotification: sendMock } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + expect(sendMock).not.toHaveBeenCalled(); + }); + + it('does not pick up first-attempt notifications (retry_count === 0)', async () => { + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); + const sendMock = jest.fn(); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS }, + { sendEventNotification: sendMock } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + expect(sendMock).not.toHaveBeenCalled(); + }); + describe('Integration tests', () => { + it('should handle high-volume notification creation with idempotency', async () => { + const executeAt = futureExecuteAt(); + const payload = { message: 'Batch test' }; + const idempotencyKey = 'batch-idempotency-1'; + + it('prevents concurrent duplicate processing via distributed lock', async () => { + const id = await insertRetryable(repository, db, 1); + let firstResolve!: (v: boolean) => void; + const barrier = new Promise((res) => { firstResolve = res; }); + const callOrder: string[] = []; + + const sendMock = jest.fn() + .mockImplementationOnce(async () => { + callOrder.push('start'); + const result = await barrier; + callOrder.push('end'); + return result; + }) + .mockResolvedValue(true); + + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + const s1 = new RetryScheduler(repository, { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, discordService); + const s2 = new RetryScheduler(repository, { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, discordService); + + const p1 = s1.runOnce(); + await s2.runOnce(); // s2 should find nothing (s1 holds the lock) + firstResolve(true); + await p1; + + expect(sendMock).toHaveBeenCalledTimes(1); + const row = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + }); + + // -- NotificationScheduler retry via scheduler loop -- + + it('retries a failed delivery on the next scheduler poll cycle', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + + const sendMock = jest.fn() + .mockResolvedValueOnce(false) // first attempt fails + .mockResolvedValue(true); // second attempt succeeds + + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + const scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); // first poll — fails + await jest.advanceTimersByTimeAsync(250); // second poll — succeeds + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(2); + + await scheduler.stop(); + jest.useRealTimers(); + }); +}); + +// =========================================================================== +// 4. FAILURE HANDLING +// =========================================================================== + +describe('4. Failure handling', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await clearTables(db); + }); + + it('marks a notification FAILED after exhausting all retries', async () => { + const id = await insertRetryable(repository, db, 3, null, 3); // retryCount === maxRetries + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ status: string; retry_count: number }>( + 'SELECT status, retry_count FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.FAILED); + }); + + it('logs a permanent-failure error after max retries are exhausted', async () => { + const id = await insertRetryable(repository, db, 3, null, 3); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + expect(logger.error).toHaveBeenCalledWith( + 'Notification permanently failed after max retries', + expect.objectContaining({ id, totalAttempts: 3 }) + ); + }); + + it('writes a FAILED execution log on permanent failure', async () => { + const id = await insertRetryable(repository, db, 3, null, 3); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id] + ); + expect(logs.some((l: any) => l.status === 'FAILED')).toBe(true); + }); + + it('handles a delivery that throws (network error) and schedules a retry', async () => { + const id = await insertRetryable(repository, db, 1, null, 3); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockRejectedValue(new Error('ECONNRESET')) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ status: string; retry_count: number; next_retry_at: string | null }>( + 'SELECT status, retry_count, next_retry_at FROM scheduled_notifications WHERE id = ?', [id] + ); + // Still pending — will be retried later + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.retry_count).toBe(2); + expect(row!.next_retry_at).not.toBeNull(); + }); + + it('transitions scheduler-dispatched notification to FAILED when retries run out', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + + const sendMock = jest.fn().mockResolvedValue(false); + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + const scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(0) // no retries — fails immediately + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.FAILED); + + await scheduler.stop(); + jest.useRealTimers(); + }); + + it('recovers stale PROCESSING locks left by crashed schedulers', async () => { + // Insert a notification stuck in PROCESSING with an expired lock + const expiredLock = new Date(Date.now() - 120_000); // 2 min ago + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + await db.run( + `UPDATE scheduled_notifications + SET status = 'PROCESSING', processor_id = 'crashed-proc', + lock_expires_at = ?, processing_started_at = ? + WHERE id = ?`, + [expiredLock.toISOString(), expiredLock.toISOString(), id] + ); + + // recoverStaleLocks should reset it back to PENDING + await repository.recoverStaleLocks('recovery-req'); + + const row = await db.get<{ status: string; processor_id: string | null }>( + 'SELECT status, processor_id FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.processor_id).toBeNull(); + }); + + it('does not recover a PROCESSING lock that has not yet expired', async () => { + const activeLock = new Date(Date.now() + 60_000); // still valid for 1 min + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + await db.run( + `UPDATE scheduled_notifications + SET status = 'PROCESSING', processor_id = 'active-proc', + lock_expires_at = ?, processing_started_at = ? + WHERE id = ?`, + [activeLock.toISOString(), new Date().toISOString(), id] + ); + + await repository.recoverStaleLocks('no-recovery-req'); + + const row = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.PROCESSING); + }); +}); + +// =========================================================================== +// 5. BACKPRESSURE & IDEMPOTENCY INTEGRATION +// =========================================================================== + +describe('5. Backpressure and idempotency integration', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let idempotencyRepo: IdempotencyKeyRepository; + let idempotencyService: IdempotencyKeyService; + let api: NotificationAPI; + let backpressureController: BackpressureController; + let backpressureMonitor: BackpressureMonitor; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + idempotencyRepo = new IdempotencyKeyRepository(db); + idempotencyService = new IdempotencyKeyService(idempotencyRepo); + api = new NotificationAPI(repository, idempotencyService); + backpressureController = new BackpressureController({ + saturationThreshold: 100, + recoveryThreshold: 50, + normalThroughputPerSec: 100, + backpressureThroughputPerSec: 10, + }); + backpressureMonitor = new BackpressureMonitor(db); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + await clearTables(db); + backpressureController.reset(); + }); + + afterEach(() => jest.useRealTimers()); + it('should maintain audit trail through complete lifecycle', async () => { + const executeAt = futureExecuteAt(50); + + it('activates backpressure when queue exceeds saturation threshold', () => { + const isActive = backpressureController.checkAndApplyBackpressure(101); + expect(isActive).toBe(true); + expect(backpressureController.isActive()).toBe(true); + + const metrics = backpressureController.getMetrics(101); + expect(metrics.isActive).toBe(true); + expect(metrics.targetThroughputPerSec).toBe(10); + }); + await scheduler.start(); + await waitForSchedulerPolls(400); + + it('calculates a positive processing delay under backpressure', () => { + backpressureController.checkAndApplyBackpressure(101); + const delay = backpressureController.calculateProcessingDelay(); + expect(delay).toBeGreaterThan(0); + }); + + it('deactivates backpressure once the queue drops below recovery threshold', () => { + backpressureController.checkAndApplyBackpressure(101); + expect(backpressureController.isActive()).toBe(true); + + backpressureController.checkAndApplyBackpressure(49); + expect(backpressureController.isActive()).toBe(false); + + const metrics = backpressureController.getMetrics(49); + expect(metrics.targetThroughputPerSec).toBe(100); + }); + + it('records a backpressure activation event and surfaces it in statistics', async () => { + await backpressureMonitor.recordEvent({ + event_type: 'ACTIVATED', + queue_size: 101, + target_throughput_per_sec: 10, + reason: 'Queue saturation', + timestamp: new Date().toISOString(), + }); + + const stats = await backpressureMonitor.getStatistics(); + expect(stats.totalActivations).toBe(1); + + const recent = await backpressureMonitor.getRecentEvents(5); + expect(recent[0].event_type).toBe('ACTIVATED'); + expect(recent[0].queue_size).toBe(101); + }); + + it('computes average duration across activation/deactivation pairs', async () => { + await backpressureMonitor.recordEvent({ + event_type: 'ACTIVATED', + queue_size: 101, + target_throughput_per_sec: 10, + timestamp: new Date().toISOString(), + }); + await backpressureMonitor.recordEvent({ + event_type: 'DEACTIVATED', + queue_size: 49, + target_throughput_per_sec: 100, + duration_ms: 5_000, + timestamp: new Date().toISOString(), + }); + + const stats = await backpressureMonitor.getStatistics(); + expect(stats.totalDeactivations).toBe(1); + expect(stats.averageDurationMs).toBe(5_000); + }); + + it('cleans up expired idempotency keys after 24 hours', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const key = 'idem-expiry-1'; + + await api.scheduleNotification( + { payload: { message: 'expiry test' }, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + + let stats = await idempotencyRepo.getStats(); + expect(stats.total).toBe(1); + + // Advance past 24-hour expiry window + jest.setSystemTime(new Date('2026-06-25T13:01:00.000Z')); + await idempotencyService.cleanupExpiredKeys(); + + stats = await idempotencyRepo.getStats(); + expect(stats.expired).toBeGreaterThanOrEqual(0); // key now expired + }); +}); diff --git a/_v531.txt b/_v531.txt new file mode 100644 index 00000000..034de81b --- /dev/null +++ b/_v531.txt @@ -0,0 +1,385 @@ +/** + * End-to-end tests for the complete notification flow: + * creation → idempotency checking → processing → delivery → audit logging + * Also covers backpressure handling under load. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { Database } from '../database/database'; +import { ScheduledNotificationRepository } from '../services/scheduled-notification-repository'; +import { IdempotencyKeyRepository } from '../services/idempotency-key-repository'; +import { IdempotencyKeyService } from '../services/idempotency-key-service'; +import { NotificationAPI } from '../services/notification-api'; +import { NotificationScheduler } from '../services/notification-scheduler'; +import { DiscordNotificationService } from '../services/discord-notification'; +import { BackpressureController } from '../services/backpressure-controller'; +import { BackpressureMonitor } from '../services/backpressure-monitor'; +import { resetWorkerManager } from '../services/worker-manager'; +import { NotificationStatus, NotificationType } from '../types/scheduled-notification'; + +describe('Notification flow end-to-end (e2e)', () => { + const testDbPath = './data/test-notification-flow-e2e.db'; + let db: Database; + let repository: ScheduledNotificationRepository; + let idempotencyRepo: IdempotencyKeyRepository; + let idempotencyService: IdempotencyKeyService; + let api: NotificationAPI; + let scheduler: NotificationScheduler; + let backpressureController: BackpressureController; + let backpressureMonitor: BackpressureMonitor; + let sendEventMock: jest.Mock; + + const schedulerConfig = { + enabled: true, + pollIntervalMs: 100, + lockTimeoutMs: 30000, + batchSize: 10, + timingBufferMs: 0, + processorId: 'e2e-processor', + }; + + beforeAll(async () => { + const dbDir = path.dirname(testDbPath); + if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true }); + if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath); + + db = new Database(testDbPath); + await db.initialize(); + repository = new ScheduledNotificationRepository(db); + idempotencyRepo = new IdempotencyKeyRepository(db); + idempotencyService = new IdempotencyKeyService(idempotencyRepo); + api = new NotificationAPI(repository, idempotencyService); + backpressureController = new BackpressureController({ + saturationThreshold: 100, + recoveryThreshold: 50, + normalThroughputPerSec: 100, + backpressureThroughputPerSec: 10, + }); + backpressureMonitor = new BackpressureMonitor(db); + }); + + afterAll(async () => { + await scheduler?.stop(); + await db.close(); + if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath); + }); + + beforeEach(async () => { + resetWorkerManager(); + jest.clearAllMocks(); + + // Clear tables + await db.run('DELETE FROM notification_execution_log'); + await db.run('DELETE FROM scheduled_notifications'); + await db.run('DELETE FROM idempotency_keys'); + await db.run('DELETE FROM backpressure_events'); + + sendEventMock = jest.fn().mockResolvedValue(true); + const discordService = { + sendEventNotification: sendEventMock, + } as unknown as DiscordNotificationService; + + scheduler = new NotificationScheduler(repository, schedulerConfig, discordService); + backpressureController.reset(); + }); + + afterEach(async () => { + await scheduler.stop(); + }); + + async function waitForSchedulerPolls(ms = 300): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); + } + + function futureExecuteAt(offsetMs = 10000): Date { + return new Date(Date.now() + offsetMs); + } + + describe('Complete notification lifecycle', () => { + it('should create, process, and deliver a notification', async () => { + const executeAt = futureExecuteAt(50); + + const id = await api.scheduleNotification({ + payload: { message: 'Test notification' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + maxRetries: 2, + }); + + expect(id).toBeGreaterThan(0); + + let notification = await repository.getById(id); + expect(notification).toBeTruthy(); + expect(notification!.status).toBe(NotificationStatus.PENDING); + + await scheduler.start(); + await waitForSchedulerPolls(400); + + notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.COMPLETED); + expect(sendEventMock).toHaveBeenCalledTimes(1); + }); + + it('should log execution attempts for audit trail', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'Test notification' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt: futureExecuteAt(50), + }); + + await scheduler.start(); + await waitForSchedulerPolls(400); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id] + ); + + expect(logs).toHaveLength(1); + expect(logs[0].status).toBe('SUCCESS'); + expect(logs[0].execution_attempt).toBe(1); + expect(logs[0].scheduled_notification_id).toBe(id); + }); + }); + + describe('Idempotency handling', () => { + it('should return cached response for duplicate requests with same idempotency key', async () => { + const executeAt = futureExecuteAt(); + const payload = { message: 'Unique message' }; + const idempotencyKey = 'test-idempotency-key-1'; + + const id1 = await api.scheduleNotification( + { + payload, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ); + + // Second request with same idempotency key should return cached response + const id2 = await api.scheduleNotification( + { + payload, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ); + + expect(id1).toBe(id2); + + // Verify only one notification was created + const stats = await repository.getStats(); + expect(stats.pending).toBe(1); + }); + + it('should reject duplicate requests with different payload', async () => { + const executeAt = futureExecuteAt(); + const idempotencyKey = 'test-idempotency-key-2'; + + await api.scheduleNotification( + { + payload: { message: 'Original' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ); + + // Different payload with same key should fail + await expect( + api.scheduleNotification( + { + payload: { message: 'Different' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ) + ).rejects.toThrow('Idempotency key reused with different request body'); + }); + + it('should clean up expired idempotency keys', async () => { + const executeAt = futureExecuteAt(); + const idempotencyKey = 'test-idempotency-key-3'; + + await api.scheduleNotification( + { + payload: { message: 'Test' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ); + + let stats = await idempotencyRepo.getStats(); + expect(stats.processed).toBe(1); + + await db.run( + 'UPDATE idempotency_keys SET expires_at = ? WHERE idempotency_key = ?', + [new Date(Date.now() - 1000).toISOString(), idempotencyKey] + ); + + const cleanupCount = await idempotencyService.cleanupExpiredKeys(); + expect(cleanupCount).toBeGreaterThanOrEqual(1); + + stats = await idempotencyRepo.getStats(); + expect(stats.expired).toBeGreaterThanOrEqual(0); + }); + }); + + describe('Backpressure handling', () => { + it('should detect queue saturation and activate backpressure', async () => { + const saturationThreshold = 100; + const isActive = backpressureController.checkAndApplyBackpressure(saturationThreshold + 1); + + expect(isActive).toBe(true); + expect(backpressureController.isActive()).toBe(true); + + const metrics = backpressureController.getMetrics(saturationThreshold + 1); + expect(metrics.isActive).toBe(true); + expect(metrics.totalBackpressureEvents).toBe(1); + }); + + it('should calculate appropriate processing delay under backpressure', async () => { + backpressureController.checkAndApplyBackpressure(101); + + const delay = backpressureController.calculateProcessingDelay(); + expect(delay).toBeGreaterThan(0); + + const metrics = backpressureController.getMetrics(101); + expect(metrics.targetThroughputPerSec).toBe(10); + }); + + it('should recover from backpressure when queue shrinks', async () => { + // Activate + backpressureController.checkAndApplyBackpressure(101); + expect(backpressureController.isActive()).toBe(true); + + // Recover + backpressureController.checkAndApplyBackpressure(49); + expect(backpressureController.isActive()).toBe(false); + + const metrics = backpressureController.getMetrics(49); + expect(metrics.targetThroughputPerSec).toBe(100); + }); + + it('should record backpressure events for audit trail', async () => { + await backpressureMonitor.recordEvent({ + event_type: 'ACTIVATED', + queue_size: 101, + target_throughput_per_sec: 10, + reason: 'Queue saturation detected', + timestamp: new Date().toISOString(), + }); + + const recent = await backpressureMonitor.getRecentEvents(10); + expect(recent).toHaveLength(1); + expect(recent[0].event_type).toBe('ACTIVATED'); + expect(recent[0].queue_size).toBe(101); + + const stats = await backpressureMonitor.getStatistics(); + expect(stats.totalActivations).toBe(1); + }); + + it('should get backpressure statistics', async () => { + await backpressureMonitor.recordEvent({ + event_type: 'ACTIVATED', + queue_size: 101, + target_throughput_per_sec: 10, + timestamp: new Date().toISOString(), + }); + + await backpressureMonitor.recordEvent({ + event_type: 'DEACTIVATED', + queue_size: 49, + target_throughput_per_sec: 100, + duration_ms: 5000, + timestamp: new Date().toISOString(), + }); + + const stats = await backpressureMonitor.getStatistics(); + expect(stats.totalActivations).toBe(1); + expect(stats.totalDeactivations).toBe(1); + expect(stats.averageDurationMs).toBe(5000); + }); + }); + + describe('Integration tests', () => { + it('should handle high-volume notification creation with idempotency', async () => { + const executeAt = futureExecuteAt(); + const payload = { message: 'Batch test' }; + const idempotencyKey = 'batch-idempotency-1'; + + // Create multiple notifications + const id1 = await api.scheduleNotification( + { + payload, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ); + + // Duplicate request + const id2 = await api.scheduleNotification( + { + payload, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }, + undefined, + idempotencyKey + ); + + expect(id1).toBe(id2); + + const stats = await repository.getStats(); + expect(stats.pending).toBe(1); + }); + + it('should maintain audit trail through complete lifecycle', async () => { + const executeAt = futureExecuteAt(50); + + const id = await api.scheduleNotification({ + payload: { message: 'Audit test' }, + notificationType: NotificationType.DISCORD, + targetRecipient: 'https://discord.com/webhook', + executeAt, + }); + + await scheduler.start(); + await waitForSchedulerPolls(400); + + // Check execution log + const executionLogs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id] + ); + + expect(executionLogs).toHaveLength(1); + expect(executionLogs[0].status).toBe('SUCCESS'); + + // Check final notification state + const notification = await repository.getById(id); + expect(notification!.status).toBe(NotificationStatus.COMPLETED); + }); + }); +}); diff --git a/_v8de.txt b/_v8de.txt new file mode 100644 index 00000000..0c5f4fba --- /dev/null +++ b/_v8de.txt @@ -0,0 +1,898 @@ +/** + * End-to-end tests for the complete notification flow. + * Issue: Core-Foundry/Notify-Chain#247 + * + * Covers every phase the issue requires: + * 1. Notification creation — input validation, scheduling, idempotency + * 2. Delivery workflows — scheduler poll → send → status update → audit log + * 3. Retry scenarios — exponential backoff, distributed lock, next_retry_at + * 4. Failure handling — max-retry exhaustion, permanent failure, stale locks + * + * The only thing mocked is the outbound network (fetch / Discord webhook). + * All other collaborators — Database, Repository, Scheduler, RetryScheduler, + * NotificationAPI, IdempotencyKeyService — run against a real in-process SQLite + * database so the full SQL path is exercised. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { Database } from '../database/database'; +import { ScheduledNotificationRepository } from '../services/scheduled-notification-repository'; +import { IdempotencyKeyRepository } from '../services/idempotency-key-repository'; +import { IdempotencyKeyService } from '../services/idempotency-key-service'; +import { NotificationAPI } from '../services/notification-api'; +import { NotificationScheduler } from '../services/notification-scheduler'; +import { RetryScheduler, RETRY_SCHEDULER_DEFAULTS, calculateBackoffDelay } from '../services/retry-scheduler'; +import { DiscordNotificationService } from '../services/discord-notification'; +import { BackpressureController } from '../services/backpressure-controller'; +import { BackpressureMonitor } from '../services/backpressure-monitor'; +import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-builder'; +import { NotificationStatus, NotificationType } from '../types/scheduled-notification'; + +jest.mock('../utils/logger', () => ({ + __esModule: true, + default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() }, +})); +jest.mock('../utils/request-id', () => ({ generateRequestId: () => 'e2e-req-id' })); + +const logger = jest.requireMock('../utils/logger').default; + +// --------------------------------------------------------------------------- +// Shared test database helpers +// --------------------------------------------------------------------------- + +const TEST_DB_PATH = './data/test-notification-flow-e2e.db'; + +async function setupDb(): Promise { + const dbDir = path.dirname(TEST_DB_PATH); + if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true }); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + const db = new Database(TEST_DB_PATH); + await db.initialize(); + return db; +} + +async function clearTables(db: Database): Promise { + await db.run('DELETE FROM notification_execution_log'); + await db.run('DELETE FROM scheduled_notifications'); + await db.run('DELETE FROM idempotency_keys'); + await db.run('DELETE FROM backpressure_events'); +} + +/** Insert a notification directly at PENDING with retry_count > 0 so + * RetryScheduler picks it up (it ignores retry_count === 0). */ +async function insertRetryable( + repo: ScheduledNotificationRepository, + db: Database, + retryCount = 1, + nextRetryAt: Date | null = null, + maxRetries = 3 +): Promise { + const id = await repo.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(maxRetries) + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + await db.run( + `UPDATE scheduled_notifications + SET status = ?, retry_count = ?, next_retry_at = ? + WHERE id = ?`, + [NotificationStatus.PENDING, retryCount, nextRetryAt?.toISOString() ?? null, id] + ); + return id; +} + +const SCHEDULER_CONFIG = { + enabled: true, + pollIntervalMs: 100, + lockTimeoutMs: 30_000, + batchSize: 10, + timingBufferMs: 0, + processorId: 'e2e-processor', +}; + +// =========================================================================== +// 1. NOTIFICATION CREATION +// =========================================================================== + +describe('1. Notification creation', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let idempotencyRepo: IdempotencyKeyRepository; + let idempotencyService: IdempotencyKeyService; + let api: NotificationAPI; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + idempotencyRepo = new IdempotencyKeyRepository(db); + idempotencyService = new IdempotencyKeyService(idempotencyRepo); + api = new NotificationAPI(repository, idempotencyService); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + await clearTables(db); + }); + + afterEach(() => jest.useRealTimers()); + + it('creates a notification and persists it as PENDING', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const id = await api.scheduleNotification({ + payload: { message: 'hello' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt, + maxRetries: 3, + }); + + expect(id).toBeGreaterThan(0); + const row = await repository.getById(id); + expect(row).toBeTruthy(); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.retryCount).toBe(0); + expect(row!.maxRetries).toBe(3); + }); + + it('rejects a notification with a past executeAt', async () => { + await expect( + api.scheduleNotification({ + payload: { message: 'too late' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T11:59:59.000Z'), // 1 second in the past + }) + ).rejects.toThrow('executeAt must be a future timestamp'); + }); + + it('rejects a notification with a missing targetRecipient', async () => { + await expect( + api.scheduleNotification({ + payload: { message: 'no recipient' }, + notificationType: NotificationType.DISCORD, + targetRecipient: '', + executeAt: new Date('2026-06-24T13:00:00.000Z'), + }) + ).rejects.toThrow('targetRecipient is required'); + }); + + it('rejects a notification with an invalid payload', async () => { + await expect( + api.scheduleNotification({ + payload: null as any, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T13:00:00.000Z'), + }) + ).rejects.toThrow('payload must be a valid object'); + }); + + it('schedules a Discord notification via the convenience helper', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const id = await api.scheduleDiscordNotification( + NotificationFixtureBuilder.constants.webhookUrl, + { text: 'convenience' }, + executeAt + ); + expect(id).toBeGreaterThan(0); + const row = await repository.getById(id); + expect(row!.notificationType).toBe(NotificationType.DISCORD); + }); + + it('stores notification metadata when provided', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const id = await api.scheduleNotification({ + payload: { message: 'with meta' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt, + metadata: { source: 'contract', contractAddress: 'CXYZ' }, + }); + + const row = await repository.getById(id); + const meta = JSON.parse(row!.metadata!); + expect(meta.source).toBe('contract'); + expect(meta.contractAddress).toBe('CXYZ'); + }); + + // -- Idempotency -- + + it('returns the same ID for duplicate requests sharing an idempotency key', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const payload = { message: 'idempotent' }; + const key = 'idem-create-1'; + + const id1 = await api.scheduleNotification( + { payload, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + const id2 = await api.scheduleNotification( + { payload, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + + expect(id1).toBe(id2); + const stats = await repository.getStats(); + expect(stats.pending).toBe(1); + }); + + it('rejects a duplicate idempotency key used with a different payload', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const key = 'idem-create-2'; + + await api.scheduleNotification( + { payload: { message: 'original' }, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + + await expect( + api.scheduleNotification( + { payload: { message: 'tampered' }, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ) + ).rejects.toThrow('Idempotency key reused with different request body'); + }); + + it('cancels a scheduled notification', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'cancel me' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T13:00:00.000Z'), + }); + + const cancelled = await api.cancelNotification(id); + expect(cancelled).toBe(true); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.CANCELLED); + }); +}); + +// =========================================================================== +// 2. DELIVERY WORKFLOWS +// =========================================================================== + +describe('2. Delivery workflows', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let api: NotificationAPI; + let scheduler: NotificationScheduler; + let sendMock: jest.Mock; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + api = new NotificationAPI(repository); + }); + + afterAll(async () => { + await scheduler?.stop(); + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + await clearTables(db); + + sendMock = jest.fn().mockResolvedValue(true); + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + }); + + afterEach(async () => { + await scheduler.stop(); + jest.useRealTimers(); + }); + + it('processes a due notification and transitions it to COMPLETED', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'deliver me' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:00:02.000Z'), + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it('does not deliver a notification before its executeAt time', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'not yet' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:10:00.000Z'), // 10 min future + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(sendMock).not.toHaveBeenCalled(); + }); + + it('delivers once executeAt elapses mid-run', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'wait for it' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:05:00.000Z'), + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(200); + expect((await repository.getById(id))!.status).toBe(NotificationStatus.PENDING); + + jest.setSystemTime(new Date('2026-06-24T12:05:01.000Z')); + await jest.advanceTimersByTimeAsync(400); + + expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it('writes a SUCCESS execution log entry after delivery', async () => { + const id = await api.scheduleNotification({ + payload: { message: 'audit this' }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt: new Date('2026-06-24T12:00:02.000Z'), + }); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', + [id] + ); + expect(logs).toHaveLength(1); + expect(logs[0].status).toBe('SUCCESS'); + expect(logs[0].execution_attempt).toBe(1); + }); + + it('processes a batch of multiple due notifications', async () => { + const executeAt = new Date('2026-06-24T12:00:02.000Z'); + const ids = await Promise.all( + Array.from({ length: 3 }, (_, i) => + api.scheduleNotification({ + payload: { message: `batch-${i}` }, + notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, + executeAt, + }) + ) + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + for (const id of ids) { + expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); + } + expect(sendMock).toHaveBeenCalledTimes(3); + }); + + it('forwards the event and contract config to the Discord service', async () => { + const event = NotificationFixtureBuilder.aStellarEvent().withId('deliver-evt-1').build(); + const contractConfig = NotificationFixtureBuilder.aContractConfig().build(); + + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withPayload({ event, contractConfig }) + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + expect(sendMock).toHaveBeenCalledWith( + expect.objectContaining({ id: 'deliver-evt-1' }), + expect.objectContaining({ address: contractConfig.address }), + expect.any(String) + ); + expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); + }); +}); + +// =========================================================================== +// 3. RETRY SCENARIOS +// =========================================================================== + +describe('3. Retry scenarios', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await clearTables(db); + }); + + // -- RetryScheduler (DB-backed) -- + + it('picks up a retryable notification and marks it COMPLETED on success', async () => { + const id = await insertRetryable(repository, db, 1); + const sendMock = jest.fn().mockResolvedValue(true); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: sendMock } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(1); + }); + + it('writes a SUCCESS execution log on retry delivery', async () => { + const id = await insertRetryable(repository, db, 1); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(true) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id] + ); + expect(logs.length).toBeGreaterThanOrEqual(1); + expect(logs.find((l: any) => l.status === 'SUCCESS')).toBeTruthy(); + }); + + it('schedules next_retry_at with exponential backoff when delivery fails', async () => { + const id = await insertRetryable(repository, db, 1); + const beforeRun = Date.now(); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, baseDelayMs: 1_000, multiplier: 2, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ retry_count: number; next_retry_at: string | null; status: string }>( + 'SELECT retry_count, next_retry_at, status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.retry_count).toBe(2); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.next_retry_at).not.toBeNull(); + + const expectedDelay = calculateBackoffDelay(1, 1_000, 2, RETRY_SCHEDULER_DEFAULTS.maxDelayMs, false); + const actualDelay = new Date(row!.next_retry_at!).getTime() - beforeRun; + expect(actualDelay).toBeGreaterThanOrEqual(expectedDelay * 0.9); + }); + + it('does not process a notification whose next_retry_at is in the future', async () => { + const futureRetry = new Date(Date.now() + 60_000); + await insertRetryable(repository, db, 1, futureRetry); + const sendMock = jest.fn(); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS }, + { sendEventNotification: sendMock } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + expect(sendMock).not.toHaveBeenCalled(); + }); + + it('does not pick up first-attempt notifications (retry_count === 0)', async () => { + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); + const sendMock = jest.fn(); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS }, + { sendEventNotification: sendMock } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + expect(sendMock).not.toHaveBeenCalled(); + }); + + it('prevents concurrent duplicate processing via distributed lock', async () => { + const id = await insertRetryable(repository, db, 1); + let firstResolve!: (v: boolean) => void; + const barrier = new Promise((res) => { firstResolve = res; }); + const callOrder: string[] = []; + + const sendMock = jest.fn() + .mockImplementationOnce(async () => { + callOrder.push('start'); + const result = await barrier; + callOrder.push('end'); + return result; + }) + .mockResolvedValue(true); + + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + const s1 = new RetryScheduler(repository, { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, discordService); + const s2 = new RetryScheduler(repository, { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, discordService); + + const p1 = s1.runOnce(); + await s2.runOnce(); // s2 should find nothing (s1 holds the lock) + firstResolve(true); + await p1; + + expect(sendMock).toHaveBeenCalledTimes(1); + const row = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + }); + + // -- NotificationScheduler retry via scheduler loop -- + + it('retries a failed delivery on the next scheduler poll cycle', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + + const sendMock = jest.fn() + .mockResolvedValueOnce(false) // first attempt fails + .mockResolvedValue(true); // second attempt succeeds + + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + const scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(3) + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); // first poll — fails + await jest.advanceTimersByTimeAsync(250); // second poll — succeeds + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.COMPLETED); + expect(sendMock).toHaveBeenCalledTimes(2); + + await scheduler.stop(); + jest.useRealTimers(); + }); +}); + +// =========================================================================== +// 4. FAILURE HANDLING +// =========================================================================== + +describe('4. Failure handling', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await clearTables(db); + }); + + it('marks a notification FAILED after exhausting all retries', async () => { + const id = await insertRetryable(repository, db, 3, null, 3); // retryCount === maxRetries + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ status: string; retry_count: number }>( + 'SELECT status, retry_count FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.FAILED); + }); + + it('logs a permanent-failure error after max retries are exhausted', async () => { + const id = await insertRetryable(repository, db, 3, null, 3); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + expect(logger.error).toHaveBeenCalledWith( + 'Notification permanently failed after max retries', + expect.objectContaining({ id, totalAttempts: 3 }) + ); + }); + + it('writes a FAILED execution log on permanent failure', async () => { + const id = await insertRetryable(repository, db, 3, null, 3); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockResolvedValue(false) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const logs = await db.all( + 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', [id] + ); + expect(logs.some((l: any) => l.status === 'FAILED')).toBe(true); + }); + + it('handles a delivery that throws (network error) and schedules a retry', async () => { + const id = await insertRetryable(repository, db, 1, null, 3); + const retryScheduler = new RetryScheduler( + repository, + { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, + { sendEventNotification: jest.fn().mockRejectedValue(new Error('ECONNRESET')) } as unknown as DiscordNotificationService + ); + + await retryScheduler.runOnce(); + + const row = await db.get<{ status: string; retry_count: number; next_retry_at: string | null }>( + 'SELECT status, retry_count, next_retry_at FROM scheduled_notifications WHERE id = ?', [id] + ); + // Still pending — will be retried later + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.retry_count).toBe(2); + expect(row!.next_retry_at).not.toBeNull(); + }); + + it('transitions scheduler-dispatched notification to FAILED when retries run out', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + + const sendMock = jest.fn().mockResolvedValue(false); + const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; + const scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withMaxRetries(0) // no retries — fails immediately + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + + await scheduler.start(); + await jest.advanceTimersByTimeAsync(250); + + const row = await repository.getById(id); + expect(row!.status).toBe(NotificationStatus.FAILED); + + await scheduler.stop(); + jest.useRealTimers(); + }); + + it('recovers stale PROCESSING locks left by crashed schedulers', async () => { + // Insert a notification stuck in PROCESSING with an expired lock + const expiredLock = new Date(Date.now() - 120_000); // 2 min ago + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + await db.run( + `UPDATE scheduled_notifications + SET status = 'PROCESSING', processor_id = 'crashed-proc', + lock_expires_at = ?, processing_started_at = ? + WHERE id = ?`, + [expiredLock.toISOString(), expiredLock.toISOString(), id] + ); + + // recoverStaleLocks should reset it back to PENDING + await repository.recoverStaleLocks('recovery-req'); + + const row = await db.get<{ status: string; processor_id: string | null }>( + 'SELECT status, processor_id FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.PENDING); + expect(row!.processor_id).toBeNull(); + }); + + it('does not recover a PROCESSING lock that has not yet expired', async () => { + const activeLock = new Date(Date.now() + 60_000); // still valid for 1 min + const id = await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput() + .forImmediateExecution() + .withTargetRecipient(NotificationFixtureBuilder.constants.webhookUrl) + .build() + ); + await db.run( + `UPDATE scheduled_notifications + SET status = 'PROCESSING', processor_id = 'active-proc', + lock_expires_at = ?, processing_started_at = ? + WHERE id = ?`, + [activeLock.toISOString(), new Date().toISOString(), id] + ); + + await repository.recoverStaleLocks('no-recovery-req'); + + const row = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + expect(row!.status).toBe(NotificationStatus.PROCESSING); + }); +}); + +// =========================================================================== +// 5. BACKPRESSURE & IDEMPOTENCY INTEGRATION +// =========================================================================== + +describe('5. Backpressure and idempotency integration', () => { + let db: Database; + let repository: ScheduledNotificationRepository; + let idempotencyRepo: IdempotencyKeyRepository; + let idempotencyService: IdempotencyKeyService; + let api: NotificationAPI; + let backpressureController: BackpressureController; + let backpressureMonitor: BackpressureMonitor; + + beforeAll(async () => { + db = await setupDb(); + repository = new ScheduledNotificationRepository(db); + idempotencyRepo = new IdempotencyKeyRepository(db); + idempotencyService = new IdempotencyKeyService(idempotencyRepo); + api = new NotificationAPI(repository, idempotencyService); + backpressureController = new BackpressureController({ + saturationThreshold: 100, + recoveryThreshold: 50, + normalThroughputPerSec: 100, + backpressureThroughputPerSec: 10, + }); + backpressureMonitor = new BackpressureMonitor(db); + }); + + afterAll(async () => { + await db.close(); + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); + await clearTables(db); + backpressureController.reset(); + }); + + afterEach(() => jest.useRealTimers()); + + it('activates backpressure when queue exceeds saturation threshold', () => { + const isActive = backpressureController.checkAndApplyBackpressure(101); + expect(isActive).toBe(true); + expect(backpressureController.isActive()).toBe(true); + + const metrics = backpressureController.getMetrics(101); + expect(metrics.isActive).toBe(true); + expect(metrics.targetThroughputPerSec).toBe(10); + }); + + it('calculates a positive processing delay under backpressure', () => { + backpressureController.checkAndApplyBackpressure(101); + const delay = backpressureController.calculateProcessingDelay(); + expect(delay).toBeGreaterThan(0); + }); + + it('deactivates backpressure once the queue drops below recovery threshold', () => { + backpressureController.checkAndApplyBackpressure(101); + expect(backpressureController.isActive()).toBe(true); + + backpressureController.checkAndApplyBackpressure(49); + expect(backpressureController.isActive()).toBe(false); + + const metrics = backpressureController.getMetrics(49); + expect(metrics.targetThroughputPerSec).toBe(100); + }); + + it('records a backpressure activation event and surfaces it in statistics', async () => { + await backpressureMonitor.recordEvent({ + event_type: 'ACTIVATED', + queue_size: 101, + target_throughput_per_sec: 10, + reason: 'Queue saturation', + timestamp: new Date().toISOString(), + }); + + const stats = await backpressureMonitor.getStatistics(); + expect(stats.totalActivations).toBe(1); + + const recent = await backpressureMonitor.getRecentEvents(5); + expect(recent[0].event_type).toBe('ACTIVATED'); + expect(recent[0].queue_size).toBe(101); + }); + + it('computes average duration across activation/deactivation pairs', async () => { + await backpressureMonitor.recordEvent({ + event_type: 'ACTIVATED', + queue_size: 101, + target_throughput_per_sec: 10, + timestamp: new Date().toISOString(), + }); + await backpressureMonitor.recordEvent({ + event_type: 'DEACTIVATED', + queue_size: 49, + target_throughput_per_sec: 100, + duration_ms: 5_000, + timestamp: new Date().toISOString(), + }); + + const stats = await backpressureMonitor.getStatistics(); + expect(stats.totalDeactivations).toBe(1); + expect(stats.averageDurationMs).toBe(5_000); + }); + + it('cleans up expired idempotency keys after 24 hours', async () => { + const executeAt = new Date('2026-06-24T13:00:00.000Z'); + const key = 'idem-expiry-1'; + + await api.scheduleNotification( + { payload: { message: 'expiry test' }, notificationType: NotificationType.DISCORD, + targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, + undefined, key + ); + + let stats = await idempotencyRepo.getStats(); + expect(stats.total).toBe(1); + + // Advance past 24-hour expiry window + jest.setSystemTime(new Date('2026-06-25T13:01:00.000Z')); + await idempotencyService.cleanupExpiredKeys(); + + stats = await idempotencyRepo.getStats(); + expect(stats.expired).toBeGreaterThanOrEqual(0); // key now expired + }); +}); diff --git a/_wh.txt b/_wh.txt new file mode 100644 index 00000000..b808d210 --- /dev/null +++ b/_wh.txt @@ -0,0 +1,159 @@ +/** + * Bounded retry helper for transient API failures. + * + * Wraps the sendWebhook function with retry logic driven by a {@link RetryPolicy}, + * so the attempt budget, the delay curve and the set of eligible failure types + * are all configurable rather than hard-coded here. + * + * The shipped default policy retries: + * - Network/connection errors + * - Timeout (AbortError) + * - HTTP 429 (Too Many Requests) + * - HTTP 500, 502, 503, 504 (Server errors) + * + * Permanent client errors (400, 401, 403, 404, 410, 422) are NOT retried: a + * later attempt would produce the identical rejection, so the response is + * returned to the caller immediately. + */ + +import { sendWebhook, WebhookSendOptions } from './webhook-sender'; +import { RetryFailureType, RetryPolicy, classifyError, classifyHttpStatus } from './retry-policy'; + +/** Maximum number of retry attempts (not counting the initial attempt). */ +const MAX_RETRY_ATTEMPTS = 2; + +/** Delay in milliseconds before the first retry. */ +const RETRY_DELAY_MS = 1000; + +export interface WebhookRetryOptions extends WebhookSendOptions { + /** + * Retry policy overrides. Defaults to + * `{ maxAttempts: 1 + MAX_RETRY_ATTEMPTS, baseDelayMs: RETRY_DELAY_MS, multiplier: 1, jitter: false }` + * so the historical behaviour is preserved unless a policy is supplied. + */ + retryPolicy?: ConstructorParameters[0]; +} + +const DEFAULT_RETRY_POLICY = new RetryPolicy({ + maxAttempts: 1 + MAX_RETRY_ATTEMPTS, + baseDelayMs: RETRY_DELAY_MS, + multiplier: 1, + maxDelayMs: RETRY_DELAY_MS, + jitter: false, +}); + +/** + * Determines if an error or response should trigger a retry. + * + * @param response - The HTTP response, if available + * @param error - The error thrown, if any + * @param policy - Policy deciding which failure types are retryable + * @returns true if the failure is retryable + */ +export function isRetryable( + response?: Response, + error?: unknown, + policy: RetryPolicy = DEFAULT_RETRY_POLICY, +): boolean { + // An explicit status code is the most precise signal available. + if (response) { + if (response.ok) { + return false; + } + return policy.isRetryable(classifyHttpStatus(response.status)); + } + + if (error !== undefined && error !== null) { + return policy.isRetryable(classifyError(error)); + } + + return false; +} + +/** + * Delay execution for the specified number of milliseconds. + * + * @param ms - Milliseconds to delay + */ +async function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Send a webhook with bounded retry logic for transient failures. + * + * Makes an initial attempt, then retries while the policy allows: while the + * attempt budget is not exhausted *and* the failure type is eligible for retry. + * Each retry is preceded by the policy's backoff delay. + * + * @param url - Target webhook URL + * @param payload - JSON-serializable payload + * @param opts - Webhook send options (timeout, headers) plus optional retry policy + * @returns Response object or throws the final error + * @throws The last error encountered after all retry attempts are exhausted + */ +export async function sendWebhookWithRetry( + url: string, + payload: any, + opts: WebhookRetryOptions = {}, +): Promise { + const { retryPolicy, ...sendOpts } = opts; + const policy = retryPolicy ? new RetryPolicy(retryPolicy) : DEFAULT_RETRY_POLICY; + + const maxAttempts = policy.resolveMaxAttempts(1 + MAX_RETRY_ATTEMPTS); + + let lastError: unknown; + let lastResponse: Response | undefined; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await sendWebhook(url, payload, sendOpts); + + // Success case + if (response.ok) { + return response; + } + + // Non-retryable failure (permanent client error) + if (!isRetryable(response, undefined, policy)) { + return response; + } + + // Retryable failure - store response and retry if attempts remain + lastResponse = response; + + if (attempt < maxAttempts) { + await delay(policy.computeDelayMs(attempt - 1)); + } + } catch (error) { + lastError = error; + + // Permanent failures are returned/raised immediately rather than retried. + if (!policy.isRetryable(classifyError(error))) { + throw error; + } + + // If this was the last attempt, throw the error + if (attempt === maxAttempts) { + throw error; + } + + // Otherwise, delay and retry + await delay(policy.computeDelayMs(attempt - 1)); + } + } + + // If we got here, we have a failed response (not an exception) + // Return the last response + if (lastResponse) { + return lastResponse; + } + + // This should not happen, but handle it gracefully + throw lastError ?? new Error('All retry attempts failed'); +} + +/** The failure types the default webhook policy considers retryable. */ +export const WEBHOOK_RETRYABLE_FAILURE_TYPES: readonly RetryFailureType[] = DEFAULT_RETRY_POLICY + .getConfig() + .retryableFailureTypes; diff --git a/_wv.txt b/_wv.txt new file mode 100644 index 00000000..9dfdd921 --- /dev/null +++ b/_wv.txt @@ -0,0 +1,60 @@ +ts-jest[ts-compiler] (WARN) src/services/webhook-verifier.test.ts:537:11 - +error TS7022: 'outcome' implicitly has type 'any' because it does not have a type annotation and is +referenced directly or indirectly in its own initializer. + +537 const outcome = verifyWebhookRequest({ +src/services/webhook-verifier.test.ts:539:5 - error TS2353: Object +literal may only specify known properties, and 'const' does not exist in type 'WebhookVerificationContext'. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~~~ +src/services/webhook-verifier.test.ts:539:11 - error TS2588: Cannot +assign to 'sig' because it is a constant. + +539 const sig = computeWebhookSignature(payload, SECRETS[1].secret); +   ~~~ +src/services/webhook-verifier.test.ts:540:11 - error TS2448: Block-scoped +variable 'outcome' used before its declaration. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ + src/services/webhook-verifier.test.ts:537:11 + 537 const outcome = verifyWebhookRequest({ +    ~~~~~~~ + 'outcome' is declared here. +src/services/webhook-verifier.test.ts:540:11 - error TS2588: Cannot +src/services/webhook-verifier.test.ts:540:11 - error TS2588: Cannot +assign to 'outcome' because it is a constant. + +540 const outcome = await verifyWebhookRequest({ +   ~~~~~~~ +src/services/webhook-verifier.test.ts:550:12 - error TS7006: Parameter +'outcome' implicitly has an 'any' type. + +550 expect(outcome.authenticated).toBe(false); +   ~~~~~~~ +src/services/webhook-verifier.test.ts:550:35 - error TS2339: Property +'toBe' does not exist on type 'Promise'. + +550 expect(outcome.authenticated).toBe(false); +   ~~~~ +src/services/webhook-verifier.test.ts:604:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +604 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + src/services/webhook-verifier.test.ts:604:20 + 604 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:605:20 - error TS2339: Property +src/services/webhook-verifier.test.ts:605:20 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +605 expect(outcome.statusCode).toBe(401); +   ~~~~~~~~~~ + src/services/webhook-verifier.test.ts:605:20 + 605 expect(outcome.statusCode).toBe(401); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:606:20 - error TS2339: Property diff --git a/contract/contracts/hello-world/contract-test-out.txt b/contract/contracts/hello-world/contract-test-out.txt new file mode 100644 index 00000000..3a606f57 --- /dev/null +++ b/contract/contracts/hello-world/contract-test-out.txt @@ -0,0 +1,844 @@ +cargo : warning: unused import: `Address as _` +At line:15 char:3390 ++ ... BUILD_JOBS="1"; cargo +1.85.0-x86_64-pc-windows-gnu test 2>&1 | Tee-O ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: ([38;5;...dress as _`:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + --> contracts\hello-world\src\tests\audit_log_test.rs:15:30 + | +15 | use soroban_sdk::testutils::{Address as _, +Events, Ledger}; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +warning: unused import: `NotificationCategory` + --> contracts\hello-world\src\tests\batch_ack_test.rs:9:27 + | +9 | use crate::base::events::{NotificationCategory, +NotificationPriority}; + | ^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `Val` + --> contracts\hello-world\src\tests\batch_ack_test.rs:14:69 + | +14 | use soroban_sdk::{Address, BytesN, Env, String, +Symbol, TryFromVal, Val, Vec}; + | +^^^ + +warning: unused import: `Val` + --> contracts\hello-world\src\tests\batch_event_test.rs:6:47 + | +6 | use soroban_sdk::{BytesN, Symbol, TryFromVal, +Val}; + | ^^^ + +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\batch_notification_test.rs:15:30 + | +15 | use soroban_sdk::testutils::{Address as _, +Events, Ledger}; + | ^^^^^^^^^^^^ + +warning: unused import: `AutoShareContract` + --> contracts\hello-world\src\tests\event_regression_test.rs:15:13 + | +15 | use crate::{AutoShareContract, +AutoShareContractClient}; + | ^^^^^^^^^^^^^^^^^ + +warning: unused import: `vec` + --> contracts\hello-world\src\tests\event_regression_test.rs:18:5 + | +18 |  vec, Address, BytesN, Env, String, Symbol, +TryIntoVal, Val, Vec, + | ^^^ + +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\notification_lifetime_test.rs:21:30 + | +21 | use soroban_sdk::testutils::{Address as _, +Ledger}; + | ^^^^^^^^^^^^ + +warning: unused import: `crate::base::errors::Error` + --> contracts\hello-world\src\tests\notification_test.rs:18:5 + | +18 | use crate::base::errors::Error; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `soroban_sdk::Val` + --> contracts\hello-world\src\tests\payload_validation_test.rs:600:13 + | +600 |  use soroban_sdk::Val; + | ^^^^^^^^^^^^^^^^ + +warning: unused import: `AutoShareContract` + --> contracts\hello-world\src\tests\storage_optimization_test.rs:64:17 + | +64 |  use crate::{AutoShareContract, +AutoShareContractClient}; + | ^^^^^^^^^^^^^^^^^ + +warning: unused imports: `BytesN`, `Env`, and `String` + --> contracts\hello-world\src\tests\storage_optimization_test.rs:65:57 + | +65 |  use soroban_sdk::{testutils::Address as _, +Address, BytesN, Env, String, Vec}; + | +^^^^^^ ^^^ ^^^^^^ + +warning: unused import: `crate::base::errors::Error` + --> contracts\hello-world\src\tests\subscription_cancellation_test.rs:10:5 + | +10 | use crate::base::errors::Error; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `Address as _` + --> contracts\hello-world\src\tests\subscription_cancellation_test.rs:15:30 + | +15 | use soroban_sdk::testutils::{Address as _, +Events}; + | ^^^^^^^^^^^^ + +warning: unused import: `crate::base::errors::Error` + --> contracts\hello-world\src\tests\template_registry_test.rs:12:9 + | +12 |  use crate::base::errors::Error; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:143:28 + | +143 |  title: +String::from_slice(&soroban_sdk::Env::default(), "Test"), + | ^^^^^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:154:28 + | +154 |  title: +String::from_slice(&soroban_sdk::Env::default(), ""), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:166:21 + | +166 |  String::from_slice(&env, +&"a".repeat(MAX_METADATA_STRING_LENGTH as usize + 1)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:180:21 + | +180 |  String::from_slice(&env, +&"d".repeat(MAX_METADATA_STRING_LENGTH as usize + 1)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:182:28 + | +182 |  title: String::from_slice(&env, +"ok"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:194:21 + | +194 |  String::from_slice(&env, +&"u".repeat(MAX_METADATA_STRING_LENGTH as usize + 1)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:196:28 + | +196 |  title: String::from_slice(&env, +"ok"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:211:31 + | +211 |  let key = String::from_slice(&env, +core::str::from_utf8(&key_bytes).unwrap()); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:212:31 + | +212 |  let val = String::from_slice(&env, +"v"); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:217:28 + | +217 |  title: String::from_slice(&env, +"ok"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:230:21 + | +230 |  String::from_slice(&env, +"priority"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:231:21 + | +231 |  String::from_slice(&env, "high"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:234:28 + | +234 |  title: String::from_slice(&env, +"Alert"), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:235:39 + | +235 |  description: +Some(String::from_slice(&env, "Body")), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:236:36 + | +236 |  data_uri: +Some(String::from_slice(&env, "ipfs://abc")), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:250:31 + | +250 |  let key = String::from_slice(&env, +core::str::from_utf8(&key_bytes).unwrap()); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:251:31 + | +251 |  let val = String::from_slice(&env, +&"x".repeat(256)); + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:256:28 + | +256 |  title: String::from_slice(&env, +&"t".repeat(256)), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:257:39 + | +257 |  description: +Some(String::from_slice(&env, &"d".repeat(256))), + | ^^^^^^^^^^ + +warning: use of deprecated associated function +`soroban_sdk::String::from_slice`: use from_str + --> contracts\hello-world\src\base\metadata_validation.rs:258:36 + | +258 |  data_uri: +Some(String::from_slice(&env, &"u".repeat(256))), + | ^^^^^^^^^^ + +warning: use of deprecated method `soroban_sdk::Env::register_contract`: use +`register` + --> contracts\hello-world\src\tests\batch_ack_test.rs:162:14 + | +162 |  &env.register_contract(None, +crate::AutoShareContract), + | ^^^^^^^^^^^^^^^^^ + +warning: unused variable: `caller` + --> contracts\hello-world\src\autoshare_logic.rs:1745:5 + | +1745 |  caller: &Address, + | ^^^^^^ help: if this +is intentional, prefix it with an underscore: `_caller` + | + = note: `#[warn(unused_variables)]` on by default + +warning: variable does not need to be mutable + --> contracts\hello-world\src\channel_logic.rs:108:9 + | +108 |  let mut subscribers = load_subscribers(&env, +&channel_id); + | ----^^^^^^^^^^^ + | | + | help: remove this `mut` + | + = note: `#[warn(unused_mut)]` on by default + +warning: function `topics_of` is never used + --> contracts\hello-world\src\tests\archive_notification_test.rs:28:4 + | +28 | fn topics_of(env: &Env, event_name: &str) -> +Option> { + | ^^^^^^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: constant `ONE_HOUR` is never used + --> contracts\hello-world\src\tests\fuzz_test.rs:14:7 + | +14 | const ONE_HOUR: u64 = 3_600; + | ^^^^^^^^ + +warning: function `data_of` is never used + --> contracts\hello-world\src\tests\revocation_test.rs:56:4 + | +56 | fn data_of(env: &Env, event_name: &str) -> +Option { + | ^^^^^^^ + +warning: function `test_revoke_notification_while_contract_paused_fails` is +never used + --> contracts\hello-world\src\tests\revocation_test.rs:244:4 + | +244 | fn +test_revoke_notification_while_contract_paused_fails() { + | +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `hello-world` (lib test) generated 42 warnings (run `cargo fix --lib -p hello-world +--tests` to apply 16 suggestions) + Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`test` +profile [unoptimized + debuginfo]]8;;\ target(s) in 34.87s + Running unittests src\lib.rs (C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\contract\target\debug\deps\hello_world-1fc302a574bfc6ef.exe) + +running 502 tests +test base::reputation::tests::test_reputation_tier_classification ... ok +test base::reputation::tests::test_reputation_score_calculation ... ok +test base::reputation::tests::test_sender_reputation_tracking ... ok +test base::metadata_validation::tests::test_long_description_invalid ... ok +test base::metadata_validation::tests::test_valid_metadata ... ok +test base::metadata_validation::tests::test_empty_title_invalid ... ok +test base::metadata_validation::tests::test_long_title_invalid ... ok +test base::metadata_validation::tests::test_long_data_uri_invalid ... ok +test base::metadata_validation::tests::test_valid_custom_fields ... ok +test base::metadata_validation::tests::test_oversized_metadata_rejected ... ok +test base::metadata_validation::tests::test_too_many_custom_fields_invalid ... ok +test tests::access_control_test::admin_only::test_configure_notification_limits_unauthorized_emits_authorization_failure_event ... ok +test tests::access_control_test::admin_only::test_add_supported_token_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_pause_random_group_creator_not_admin_panics - should panic ... ok +test tests::access_control_test::admin_only::test_register_category_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_register_category_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_add_supported_token_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_configure_notification_limits_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_configure_notification_limits_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_pause_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_remove_supported_token_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_pause_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_remove_supported_token_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_set_usage_fee_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_set_usage_fee_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_transfer_admin_authorized_succeeds ... ok +test tests::access_control_test::admin_only::test_withdraw_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_withdraw_authorized_admin_succeeds ... ok +test tests::access_control_test::admin_only::test_transfer_admin_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::admin_only::test_unpause_authorized_admin_succeeds ... ok +test tests::access_control_test::authorization_boundary::test_cancel_notification_cross_user_write_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_get_notification_owner_can_read ... ok +test tests::access_control_test::authorization_boundary::test_get_notification_cross_user_read_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_cross_user_access_emits_authorization_failure_event ... ok +test tests::access_control_test::admin_only::test_unpause_unauthorized_user_panics - should panic ... ok +test tests::access_control_test::authorization_boundary::test_get_notification_admin_can_read ... ok +test tests::access_control_test::authorization_boundary::test_revoke_notification_cross_user_write_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_notification_ownership_is_recorded_on_schedule ... ok +test tests::access_control_test::creator_only::test_activate_group_creator_succeeds ... ok +test tests::access_control_test::authorization_boundary::test_is_notification_revoked_cross_user_read_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_extend_notification_expiry_cross_user_write_is_rejected ... ok +test tests::access_control_test::authorization_boundary::test_ownership_validation_rejects_non_owner_on_write ... ok +test tests::access_control_test::creator_only::test_deactivate_group_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_only::test_activate_group_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_only::test_deactivate_group_creator_succeeds ... ok +test tests::access_control_test::creator_only::test_add_group_member_creator_succeeds ... ok +test tests::access_control_test::authorization_boundary::test_user_cannot_access_other_users_notification ... ok +test tests::access_control_test::authorization_boundary::test_ownership_validation_allows_owner_on_write ... ok +test tests::access_control_test::creator_only::test_add_group_member_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_only::test_update_members_non_creator_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_extend_notification_expiry_admin_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_unrelated_user_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_reduce_usage_unrelated_user_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_admin_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_reduce_usage_admin_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_unauthorized_emits_authorization_failure_event ... ok +test tests::access_control_test::creator_or_admin_notifications::test_extend_notification_expiry_unrelated_user_panics - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_extend_notification_expiry_creator_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_creator_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_creator_succeeds ... ok +test tests::access_control_test::creator_or_admin_notifications::test_cancel_notification_admin_succeeds ... ok +test tests::access_log_test::test_cross_user_access_rejected_for_nonexistent_notification - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_unauthorized_emits_authorization_failure_event ... ok +test tests::access_log_test::test_access_event_fails_for_nonexistent_notification - should panic ... ok +test tests::access_control_test::creator_or_admin_notifications::test_revoke_notification_unrelated_user_panics - should panic ... ok +test tests::access_log_test::test_cross_user_read_access_is_rejected - should panic ... ok +test tests::access_log_test::test_resource_ownership_is_validated_for_non_owner - should panic ... ok +test tests::access_log_test::test_resource_ownership_is_validated_for_owner ... ok +test tests::access_log_test::test_multiple_access_events_can_be_emitted ... ok +test tests::access_log_test::test_cross_user_write_access_is_rejected - should panic ... ok +test tests::access_log_test::test_access_event_emitted_for_existing_notification ... ok +test tests::audit_log_test::test_acknowledgment_recorded ... ok +test tests::audit_log_test::test_audit_log_immutability ... ok +test tests::audit_log_test::test_audit_records_for_unknown_notification_returns_empty ... ok +test tests::archive_notification_test::test_delivery_archives_notification ... ok +test tests::archive_notification_test::test_cancel_archives_notification ... ok +test tests::access_log_test::test_owner_can_modify_own_notification ... ok +test tests::audit_log_test::test_cancel_notification_creates_audit_record ... ok +test tests::audit_log_test::test_audit_records_filtered_by_notification_id ... ok +test tests::audit_log_test::test_acknowledgment_blocked_when_paused ... ok +test tests::audit_log_test::test_delivery_attempt_blocked_when_paused ... ok +test tests::access_log_test::test_owner_cannot_modify_other_users_notification - should panic ... ok +test tests::audit_log_test::test_delivery_attempt_recorded ... ok +test tests::audit_log_test::test_delivery_failure_blocked_when_paused ... ok +test tests::audit_log_test::test_full_lifecycle_audit_trail ... ok +test tests::audit_log_test::test_schedule_notification_creates_audit_record ... ok +test tests::autoshare_test::test_add_duplicate_token_fails - should panic ... ok +test tests::audit_log_test::test_expire_notification_creates_audit_record ... ok +test tests::archive_notification_test::test_expire_archives_notification ... ok +test tests::audit_log_test::test_audit_sequence_numbers_increment ... ok +test tests::audit_log_test::test_schedule_notification_emits_audit_event ... ok +test tests::autoshare_test::test_activating_already_active_group_fails - should panic ... ok +test tests::autoshare_test::test_add_and_get_supported_tokens ... ok +test tests::autoshare_test::test_add_member_to_non_existent_group - should panic ... ok +test tests::audit_log_test::test_delivery_failure_recorded ... ok +test tests::autoshare_test::test_add_supported_token_fails_already_exists - should panic ... ok +test tests::autoshare_test::test_add_group_member_success ... ok +test tests::autoshare_test::test_add_duplicate_member - should panic ... ok +test tests::autoshare_test::test_add_member_invalid_total_percentage - should panic ... ok +test tests::autoshare_test::test_admin_initialization ... ok +test tests::autoshare_test::test_add_group_member_unauthorized - should panic ... ok +test tests::autoshare_test::test_admin_functions_after_transfer ... ok +test tests::autoshare_test::test_add_multiple_members_sequentially ... ok +test tests::autoshare_test::test_admin_can_unpause ... ok +test tests::autoshare_test::test_admin_can_pause ... ok +test tests::autoshare_test::test_create_fails_invalid_usage_count_zero - should panic ... ok +test tests::autoshare_test::test_create_fails_name_too_long - should panic ... ok +test tests::autoshare_test::test_admin_can_withdraw ... ok +test tests::autoshare_test::test_admin_can_deactivate_channel_even_if_not_creator ... ok +test tests::autoshare_test::test_create_fails_unsupported_token - should panic ... ok +test tests::audit_log_test::test_batch_schedule_creates_audit_records ... ok +test tests::autoshare_test::test_create_fails_duplicate_member - should panic ... ok +test tests::autoshare_test::test_create_group_with_zero_usages_fails ... ok +test tests::autoshare_test::test_create_and_get_success ... ok +test tests::autoshare_test::test_create_fails_empty_members - should panic ... ok +test tests::autoshare_test::test_creator_can_deactivate_group ... ok +test tests::autoshare_test::test_get_admin ... ok +test tests::autoshare_test::test_get_all_groups_empty ... ok +test tests::autoshare_test::test_deactivated_channel_blocks_new_subscriptions ... ok +test tests::autoshare_test::test_create_group_with_payment ... ok +test tests::autoshare_test::test_add_member_to_inactive_group ... ok +test tests::autoshare_test::test_creator_can_activate_group ... ok +test tests::autoshare_test::test_create_fails_invalid_percentage - should panic ... ok +test tests::autoshare_test::test_get_group_members_non_existent_group - should panic ... ok +test tests::autoshare_test::test_get_groups_by_creator_empty ... ok +test tests::autoshare_test::test_get_contract_balance ... ok +test tests::autoshare_test::test_get_non_existent_fails - should panic ... ok +test tests::autoshare_test::test_initialize_with_admin ... ok +test tests::autoshare_test::test_duplicate_id_fails - should panic ... ok +test tests::autoshare_test::test_get_all_groups_includes_inactive ... ok +test tests::autoshare_test::test_is_group_active_on_nonexistent_group_fails - should panic ... ok +test tests::autoshare_test::test_deactivated_channel_preserves_historical_subscription_data ... ok +test tests::autoshare_test::test_get_group_members_empty ... ok +test tests::autoshare_test::test_get_group_members_multiple ... ok +test tests::autoshare_test::test_group_created_as_active ... ok +test tests::autoshare_test::test_is_group_member_non_existent_group - should panic ... ok +test tests::autoshare_test::test_deactivating_already_inactive_group_fails - should panic ... ok +test tests::autoshare_test::test_is_group_member ... ok +test tests::autoshare_test::test_is_subscribed_false_after_channel_deactivated ... ok +test tests::autoshare_test::test_is_subscribed_false_for_non_member ... ok +test tests::autoshare_test::test_is_group_member_true ... ok +test tests::autoshare_test::test_is_subscribed_on_nonexistent_channel_fails - should panic ... ok +test tests::autoshare_test::test_is_group_member_false ... ok +test tests::autoshare_test::test_name_too_long_rejected - should panic ... ok +test tests::autoshare_test::test_is_group_member_works_on_inactive_group ... ok +test tests::autoshare_test::test_is_subscribed_true_for_member ... ok +test tests::autoshare_test::test_name_at_max_length_succeeds ... ok +test tests::autoshare_test::test_is_subscribed_true_for_creator ... ok +test tests::autoshare_test::test_old_admin_cannot_withdraw_after_transfer - should panic ... ok +test tests::autoshare_test::test_non_admin_cannot_set_usage_fee - should panic ... ok +test tests::autoshare_test::test_get_groups_by_creator_multiple ... ok +test tests::autoshare_test::test_non_admin_cannot_withdraw - should panic ... ok +test tests::autoshare_test::test_reduce_usage_fails_no_usages_remaining - should panic ... ok +test tests::autoshare_test::test_remove_supported_token_fails_not_found - should panic ... ok +test tests::autoshare_test::test_set_usage_fee_fails_zero - should panic ... ok +test tests::autoshare_test::test_remove_non_existent_token_fails - should panic ... ok +test tests::autoshare_test::test_status_change_on_nonexistent_group_fails - should panic ... ok +test tests::autoshare_test::test_transfer_admin_unauthorized - should panic ... ok +test tests::autoshare_test::test_remove_supported_token ... ok +test tests::autoshare_test::test_set_and_get_usage_fee ... ok +test tests::autoshare_test::test_non_creator_cannot_deactivate - should panic ... ok +test tests::autoshare_test::test_transfer_admin ... ok +test tests::autoshare_test::test_get_all_groups_multiple ... ok +test tests::autoshare_test::test_update_members_fails_too_many - should panic ... ok +test tests::autoshare_test::test_non_creator_cannot_activate - should panic ... ok +test tests::autoshare_test::test_withdraw_insufficient_balance - should panic ... ok +test tests::autoshare_test::test_viewing_inactive_group_works ... ok +test tests::autoshare_test::test_too_many_members_rejected - should panic ... ok +test tests::autoshare_test::test_topup_fails_unsupported_token - should panic ... ok +test tests::autoshare_test::test_update_members_success ... ok +test tests::autoshare_test::test_update_members_invalid_percentage - should panic ... ok +test tests::autoshare_test::test_withdraw_zero_amount - should panic ... ok +test tests::autoshare_test::test_topup_fails_invalid_usage_count_zero - should panic ... ok +test tests::autoshare_test::test_update_members_unauthorized - should panic ... ok +test tests::autoshare_test::test_withdraw_negative_amount - should panic ... ok +test tests::autoshare_test::test_updating_inactive_group_fails - should panic ... ok +test tests::batch_ack_test::test_acknowledge_revoked_fails - should panic ... ok +test tests::batch_notification_test::test_batch_duplicate_id_rejected ... ok +test tests::batch_event_test::test_emit_batch_processing_completed_event ... ok +test tests::batch_ack_test::test_acknowledge_multiple_notifications ... ok +test tests::batch_notification_test::test_batch_all_or_nothing_on_validation_failure ... ok +test tests::batch_notification_test::test_batch_blocked_when_contract_paused ... ok +test tests::autoshare_test::test_members_at_max_succeeds ... ok +test tests::batch_ack_test::test_acknowledge_expired_fails - should panic ... ok +test tests::batch_ack_test::test_acknowledge_unauthorized_fails - should panic ... ok +test tests::batch_notification_test::test_batch_exceeding_max_size_rejected ... ok +test tests::batch_notification_test::test_batch_empty_ids_rejected ... ok +test tests::batch_notification_test::test_batch_mismatched_lengths_rejected ... ok +test tests::batch_notification_test::test_batch_zero_ttl_rejected ... ok +test tests::batch_notification_test::test_batch_creates_all_notifications ... ok +test tests::batch_notification_test::test_batch_id_already_scheduled_rejected ... ok +test tests::category_registry_test::test_admin_can_register_category ... ok +test tests::category_registry_test::test_default_registry_has_defaults ... ok +test tests::batch_notification_test::test_batch_single_notification ... ok +test tests::category_registry_test::test_registry_queries_multiple_categories ... ok +test tests::category_registry_test::test_register_category_emits_event ... ok +test tests::category_registry_test::test_non_admin_cannot_register_category - should panic ... ok +test tests::batch_notification_test::test_batch_emits_per_notification_events ... ok +test tests::category_registry_test::test_duplicate_category_registration_rejected - should panic ... ok +test tests::batch_notification_test::test_batch_notifications_expire_correctly ... ok +test tests::batch_notification_test::test_batch_summary_event_has_notification_category ... ok +test tests::channel_metadata_test::test_update_channel_metadata_emits_event ... ok +test tests::channel_subscription_test::test_batch_subscribe_empty_rejected ... ok +test tests::batch_notification_test::test_batch_emits_summary_event ... ok +test tests::channel_metadata_test::test_update_channel_metadata_rejects_non_creator - should panic ... ok +test tests::channel_subscription_test::test_batch_subscribe_partial_failure_does_not_corrupt_state ... ok +test tests::channel_subscription_test::test_get_channel_creator_not_found ... ok +test tests::channel_subscription_test::test_batch_subscribe_too_large_rejected ... ok +test tests::channel_subscription_test::test_batch_subscribe_multiple_channels ... ok +test tests::channel_subscription_test::test_create_channel_stores_creator ... ok +test tests::event_regression_test::test_admin_transferred_event_structure ... ok +test tests::channel_subscription_test::test_duplicate_subscribe_rejected_count_unchanged ... ok +test tests::channel_metadata_test::test_update_channel_metadata_stores_description ... ok +test tests::batch_ack_test::benchmark_gas_usage ... ok +test tests::channel_metadata_test::test_update_channel_metadata_leaves_subscribers_intact ... ok +test tests::channel_subscription_test::test_subscriber_count_updates_on_subscribe_and_unsubscribe ... ok +test tests::event_regression_test::test_contract_paused_event_no_data_payload ... ok +test tests::event_regression_test::test_event_names_are_stable ... ok +test tests::event_regression_test::test_admin_transferred_event_topic_ordering ... ok +test tests::event_regression_test::test_contract_paused_event_structure ... ok +test tests::event_regression_test::test_contract_unpaused_event_structure ... ok +test tests::event_regression_test::test_autoshare_created_event_structure ... ok +test tests::event_regression_test::test_multiple_events_in_sequence ... ok +test tests::event_regression_test::test_no_event_on_read_operations ... ok +test tests::event_regression_test::test_autoshare_created_event_topic_ordering ... ok +test tests::expiration_test::test_cannot_extend_nonexistent_notification - should panic ... ok +test tests::event_regression_test::test_withdrawal_event_structure ... ok +test tests::event_regression_test::test_withdrawal_event_topic_ordering ... ok +test tests::expiration_test::test_cannot_extend_by_zero_seconds - should panic ... ok +test tests::expiration_test::test_cannot_extend_expired_notification - should panic ... ok +test tests::expiration_test::test_cancellation_blocked_when_contract_paused ... ok +test tests::event_regression_test::test_autoshare_updated_event_structure ... ok +test tests::expiration_test::test_expire_before_deadline_is_rejected ... ok +test tests::expiration_test::test_cancelling_untracked_id_still_emits_event ... ok +test tests::expiration_test::test_expire_unknown_notification_fails ... ok +test tests::expiration_test::test_cannot_extend_revoked_notification - should panic ... ok +test tests::event_regression_test::test_group_activated_event_structure ... ok +test tests::expiration_test::test_get_unknown_notification_fails ... ok +test tests::expiration_test::test_extend_notification_expiry_by_admin ... ok +test tests::expiration_test::test_extend_notification_expiry_while_contract_paused_fails - should panic ... ok +test tests::expiration_test::test_duplicate_schedule_is_rejected ... ok +test tests::expiration_test::test_not_expired_before_deadline_and_expired_after ... ok +test tests::expiration_test::test_expired_notification_cannot_be_cancelled ... ok +test tests::expiration_test::test_zero_duration_is_rejected ... ok +test tests::expiration_test::test_expire_after_deadline_emits_event_and_reaps_storage ... ok +test tests::expiration_test::test_schedule_blocked_when_contract_paused ... ok +test tests::event_regression_test::test_group_deactivated_event_structure ... ok +test tests::expiration_test::test_extend_notification_expiry_by_creator ... ok +test tests::expiration_test::test_valid_notification_can_be_cancelled_and_emits_event ... ok +test tests::extended_coverage_test::test_acknowledgment_on_expired_notification_rejected ... ok +test tests::extended_coverage_test::test_audit_record_appended_event_has_notification_category ... ok +test tests::extended_coverage_test::test_batch_ttl_overflow_rejected ... ok +test tests::extended_coverage_test::test_category_registered_event_carries_category_and_priority ... ok +test tests::extended_coverage_test::test_audit_records_per_notification_match_full_log_subset ... ok +test tests::extended_coverage_test::test_configure_limits_non_admin_rejected ... ok +test tests::extended_coverage_test::test_configure_limits_valid_values_accepted ... ok +test tests::expiration_test::test_schedule_stores_created_and_expiry ... ok +test tests::expiration_test::test_schedule_emits_notification_scheduled_event ... ok +test tests::extended_coverage_test::test_configure_limits_min_greater_than_max_rejected ... ok +test tests::extended_coverage_test::test_configure_limits_zero_batch_size_rejected ... ok +test tests::extended_coverage_test::test_audit_record_appended_carries_correct_action_topic ... ok +test tests::extended_coverage_test::test_audit_log_seq_starts_at_one ... ok +test tests::extended_coverage_test::test_audit_log_grows_across_independent_notifications ... ok +test tests::extended_coverage_test::test_configure_limits_emits_event_with_admin_category ... ok +test tests::extended_coverage_test::test_notification_expired_event_has_notification_category ... ok +test tests::extended_coverage_test::test_notification_recalled_event_has_notification_category ... ok +test tests::extended_coverage_test::test_delivery_attempt_on_revoked_notification_rejected ... ok +test tests::extended_coverage_test::test_ownership_transfer_events_have_admin_category ... ok +test tests::extended_coverage_test::test_record_acknowledgment_on_unknown_id_rejected ... ok +test tests::extended_coverage_test::test_schema_version_set_has_admin_category ... ok +test tests::extended_coverage_test::test_record_delivery_failure_on_unknown_id_rejected ... ok +test tests::expiration_test::test_extend_notification_expiry_by_unauthorized_user_fails - should panic ... ok +test tests::extended_coverage_test::test_revoke_notification_event_has_notification_category ... ok +test tests::extended_coverage_test::test_record_delivery_attempt_on_unknown_id_rejected ... ok +test tests::extended_coverage_test::test_notification_accessed_has_notification_category ... ok +test tests::extended_coverage_test::test_notification_delivered_event_has_notification_category ... ok +test tests::metadata_validation_test::test_batch_rejects_empty_title - should panic ... ok +test tests::extended_coverage_test::test_subscription_cancelled_has_group_category ... ok +test tests::fuzz_test::fuzz_reduce_usage_never_exceeds_paid_total ... ok +test tests::mock_token_test::test_insufficient_balance - should panic ... ok +test tests::mock_token_test::test_invalid_mint_amount - should panic ... ok +test tests::metadata_validation_test::test_schedule_rejects_empty_title - should panic ... ok +test tests::mock_token_test::test_invalid_transfer_amount - should panic ... ok +test tests::metadata_validation_test::test_schedule_accepts_valid_title ... ok +test tests::notification_lifetime_test::test_batch_schedule_zero_lifetime_rejected ... ok +test tests::mock_token_test::test_mock_token ... ok +test tests::notification_lifetime_test::test_extend_by_absurd_seconds_rejected ... ok +test tests::notification_lifetime_test::test_batch_schedule_one_second_over_max_rejected ... ok +test tests::notification_lifetime_test::test_extend_by_zero_seconds_rejected ... ok +test tests::notification_lifetime_test::test_extend_one_second_over_max_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_schedule_at_max_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_schedule_minimum_valid_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_batch_schedule_at_max_lifetime_succeeds ... ok +test tests::notification_lifetime_test::test_schedule_one_second_over_max_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_schedule_absurd_lifetime_rejected ... ok +test tests::notification_lifetime_test::test_schedule_typical_one_hour_lifetime_succeeds ... ok +test tests::notification_test::test_batch_cancel_rejects_cross_user_access ... ok +test tests::notification_lifetime_test::test_schedule_zero_lifetime_rejected ... ok +test tests::notification_test::test_admin_transfer_event_has_admin_category ... ok +test tests::notification_lifetime_test::test_extend_to_exactly_max_lifetime_succeeds ... ok +test tests::notification_test::test_batch_schedule_notifications_stores_per_item_priority ... ok +test tests::notification_test::test_cancellation_event_has_notification_category ... ok +test tests::notification_test::test_cancel_notification_rejects_cross_user_write ... ok +test tests::notification_test::test_cancellation_event_is_emitted ... ok +test tests::notification_test::test_cancel_notification_allows_owner ... ok +test tests::notification_test::test_cancellation_blocked_when_contract_paused ... ok +test tests::notification_test::test_cancellation_event_data_contains_notification_id ... ok +test tests::notification_test::test_cancellation_event_topic_shape ... ok +test tests::notification_test::test_concurrent_duplicate_delivery_requests_are_safe ... ok +test tests::notification_test::test_created_event_has_group_category ... ok +test tests::notification_test::test_created_event_backward_compatible_shape ... ok +test tests::notification_test::test_confirm_delivery_rejects_cross_user_write ... ok +test tests::notification_test::test_confirm_delivery_is_idempotent_for_repeated_requests ... ok +test tests::notification_test::test_get_notification_rejects_cross_user_read ... ok +test tests::notification_test::test_created_group_stores_standard_priority ... ok +test tests::notification_test::test_delivery_idempotency_state_is_persisted ... ok +test tests::notification_test::test_multiple_cancellations_emit_distinct_events ... ok +test tests::notification_test::test_deactivate_and_activate_events_have_group_category ... ok +test tests::notification_test::test_pause_and_unpause_events_have_admin_category ... ok +test tests::notification_test::test_events_can_be_filtered_by_category ... ok +test tests::notification_test::test_recall_notification_rejects_unauthorized_sender ... ok +test tests::notification_test::test_recall_notification_rejects_cross_user_write ... ok +test tests::notification_test::test_notification_ownership_is_recorded_and_enforced ... ok +test tests::notification_test::test_recall_notification_emits_event_for_sender ... ok +test tests::notification_test::test_schedule_notification_event_carries_assigned_priority ... ok +test tests::notification_test::test_recall_notification_rejects_after_delivery_confirmation ... ok +test tests::notification_validation_test::test_activate_already_active_group_is_rejected - should panic ... ok +test tests::notification_test::test_schedule_notification_stores_assigned_priority ... ok +test tests::notification_validation_test::test_add_supported_token_duplicate_is_rejected - should panic ... ok +test tests::notification_validation_test::test_activate_group_while_paused_is_rejected - should panic ... ok +test tests::notification_test::test_updated_event_has_group_category ... ok +test tests::notification_test::test_withdrawal_event_has_financial_category ... ok +test tests::notification_validation_test::test_create_with_unsupported_token_is_rejected - should panic ... ok +test tests::notification_validation_test::test_create_zero_usage_count_is_rejected - should panic ... ok +test tests::notification_validation_test::test_create_name_one_over_max_length_is_rejected - should panic ... ok +test tests::notification_validation_test::test_create_name_at_max_length_is_accepted ... ok +test tests::notification_validation_test::test_remove_supported_token_not_found_is_rejected - should panic ... ok +test tests::notification_validation_test::test_deactivate_group_while_paused_is_rejected - should panic ... ok +test tests::fuzz_test::fuzz_paused_contract_blocks_group_creation ... ok +test tests::notification_validation_test::test_set_usage_fee_by_non_admin_is_rejected - should panic ... ok +test tests::notification_validation_test::test_reduce_usage_nonexistent_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_deactivate_already_inactive_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_set_usage_fee_zero_is_rejected - should panic ... ok +test tests::notification_validation_test::test_topup_nonexistent_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_create_payload_persisted_correctly_after_event ... ok +test tests::notification_validation_test::test_unauthorized_activate_emits_authorization_failure_with_admin_category - should panic ... ok +test tests::notification_validation_test::test_unauthorized_deactivate_emits_authorization_failure_with_admin_category - should panic ... ok +test tests::notification_validation_test::test_reduce_usage_below_zero_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_51_members_is_rejected - should panic ... ok +test tests::notification_validation_test::test_unauthorized_update_emits_authorization_failure_with_admin_category - should panic ... ok +test tests::notification_validation_test::test_topup_unsupported_token_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_nonexistent_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_inactive_group_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_empty_list_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_percentages_sum_to_101_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_valid_payload_emits_group_category ... ok +test tests::notification_validation_test::test_update_members_exactly_50_members_is_valid ... ok +test tests::notification_validation_test::test_update_members_duplicate_address_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_while_paused_is_rejected - should panic ... ok +test tests::notification_validation_test::test_update_members_single_member_100_percent_is_valid ... ok +test tests::fuzz_test::fuzz_zero_usage_count_always_rejected ... ok +test tests::notification_validation_test::test_withdrawal_exceeds_balance_is_rejected - should panic ... ok +test tests::notification_version_test::test_current_notification_version_is_documented ... ok +test tests::notification_validation_test::test_update_members_percentages_sum_to_99_is_rejected - should panic ... ok +test tests::notification_validation_test::test_withdrawal_event_carries_correct_amount_and_financial_category ... ok +test tests::notification_validation_test::test_withdrawal_negative_amount_is_rejected - should panic ... ok +test tests::notification_validation_test::test_topup_zero_additional_usages_is_rejected - should panic ... ok +test tests::ownership_transfer_test::test_accept_ownership_fails_when_no_transfer_pending - should panic ... ok +test tests::notification_version_test::test_scheduled_notification_includes_version ... ok +test tests::notification_version_test::test_batch_scheduled_notifications_include_version ... ok +test tests::ownership_transfer_test::test_initiate_and_accept_ownership_transfer ... ok +test tests::ownership_transfer_test::test_new_owner_can_exercise_admin_rights_after_transfer ... ok +test tests::ownership_transfer_test::test_current_owner_unchanged_until_acceptance ... ok +test tests::notification_validation_test::test_withdrawal_zero_amount_is_rejected - should panic ... ok +test tests::ownership_transfer_test::test_self_transfer_rejected_by_initiate - should panic ... ok +test tests::ownership_transfer_test::test_get_pending_owner_returns_none_initially ... ok +test tests::ownership_transfer_test::test_non_owner_cannot_initiate_ownership_transfer - should panic ... ok +test tests::ownership_transfer_test::test_transfer_admin_non_owner_rejected - should panic ... ok +test tests::ownership_transfer_test::test_owner_can_change_pending_owner_before_acceptance ... ok +test tests::pause_test::test_admin_can_pause ... ok +test tests::ownership_transfer_test::test_pending_owner_cleared_after_acceptance ... ok +test tests::pause_test::test_admin_can_unpause ... ok +test tests::ownership_transfer_test::test_transfer_admin_rejects_self_transfer - should panic ... ok +test tests::ownership_transfer_test::test_transfer_admin_still_works ... ok +test tests::ownership_transfer_test::test_pending_owner_set_after_initiation ... ok +test tests::batch_notification_test::test_batch_exactly_max_size_accepted ... ok +test tests::pause_test::test_non_admin_pause_returns_unauthorized_error ... ok +test tests::pause_test::test_get_paused_status_view_works_before_admin_initialization ... ok +test tests::pause_test::test_non_admin_unpause_returns_unauthorized_error ... ok +test tests::ownership_transfer_test::test_wrong_address_cannot_accept_ownership - should panic ... ok +test tests::pause_test::test_pause_already_paused_returns_already_paused_error ... ok +test tests::pause_test::test_initial_state_is_unpaused_after_deployment ... ok +test tests::pause_test::test_create_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_pause_emits_event_with_admin_actor ... ok +test tests::pause_test::test_repeated_state_transitions_active_paused_active_paused ... ok +test tests::pause_test::test_paused_status_returned_correctly ... ok +test tests::pause_test::test_unpause_emits_event_with_admin_actor ... ok +test tests::pause_test::test_transfer_admin_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_unpause_already_active_returns_not_paused_error ... ok +test tests::pause_test::test_read_functions_work_when_paused ... ok +test tests::pause_test::test_add_member_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_deactivate_group_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_update_members_returns_contract_paused_error_when_paused ... ok +test tests::pause_test::test_operations_work_after_unpause ... ok +test tests::payload_validation_test::test_create_rejects_name_over_max_length ... ok +test tests::payload_validation_test::test_create_rejects_duplicate_id ... ok +test tests::payload_validation_test::test_create_rejects_unsupported_token ... ok +test tests::fuzz_test::fuzz_notification_zero_ttl_rejected ... ok +test tests::payload_validation_test::test_admin_transfer_has_critical_priority ... ok +test tests::pause_test::test_state_preserved_after_unpause_cycle ... ok +test tests::payload_validation_test::test_admin_events_have_admin_category ... ok +test tests::payload_validation_test::test_consumer_can_filter_by_category ... ok +test tests::payload_validation_test::test_schedule_rejects_duplicate_id ... ok +test tests::payload_validation_test::test_create_rejects_zero_usage_count ... ok +test tests::payload_validation_test::test_notification_events_have_notification_category ... ok +test tests::pause_test::test_topup_subscription_returns_contract_paused_error_when_paused ... ok +test tests::payload_validation_test::test_financial_events_have_financial_category ... ok +test tests::payload_validation_test::test_schedule_rejects_overflow_ttl ... ok +test tests::payload_validation_test::test_schedule_rejects_zero_ttl ... ok +test tests::payload_validation_test::test_group_events_have_group_category ... ok +test tests::payload_validation_test::test_ttl_of_one_second_is_valid ... ok +test tests::payload_validation_test::test_update_members_rejects_duplicate_addresses ... ok +test tests::payload_validation_test::test_update_members_rejects_empty_list ... ok +test tests::preferences_test::preferences_tests::test_other_user_cannot_set_channel_preference - should panic ... ok +test tests::payload_validation_test::test_usage_fee_boundary_one_is_valid ... ok +test tests::preferences_test::preferences_tests::test_get_preferences_returns_defaults_for_new_user ... ok +test tests::payload_validation_test::test_usage_fee_zero_is_rejected ... ok +test tests::payload_validation_test::test_every_event_carries_category_and_priority ... ok +test tests::preferences_test::preferences_tests::test_set_category_preference_disables_system_alerts ... ok +test tests::payload_validation_test::test_update_members_rejects_over_max_members ... ok +test tests::payload_validation_test::test_update_members_rejects_percentages_not_summing_to_100 ... ok +test tests::payload_validation_test::test_single_member_at_100_percent_is_valid ... ok +test tests::preferences_test::preferences_tests::test_set_category_preference_disables_payment_notifications ... ok +test tests::preferences_test::preferences_tests::test_preferences_persist_after_page_refresh ... ok +test tests::preferences_test::preferences_tests::test_set_channel_preference_disables_email ... ok +test tests::preferences_test::preferences_tests::test_reset_preferences_restores_all_defaults ... ok +test tests::preferences_test::preferences_tests::test_preferences_are_per_recipient ... ok +test tests::preferences_test::preferences_tests::test_ui_can_read_all_channel_states ... ok +test tests::revocation_test::test_cannot_cancel_revoked_notification - should panic ... ok +test tests::preferences_test::preferences_tests::test_set_channel_preference_re_enables_channel ... ok +test tests::preferences_test::preferences_tests::test_set_channel_preference_emits_channel_preference_updated ... ok +test tests::preferences_test::preferences_tests::test_set_preferences_updates_channels_and_categories ... ok +test tests::revocation_test::test_is_notification_revoked_after_revocation ... ok +test tests::revocation_test::test_revocation_stores_timestamp ... ok +test tests::preferences_test::preferences_tests::test_set_preferences_emits_channel_events_for_each_channel ... ok +test tests::revocation_test::test_cannot_revoke_nonexistent_notification - should panic ... ok +test tests::revocation_test::test_cannot_expire_revoked_notification - should panic ... ok +test tests::revocation_test::test_cannot_revoke_expired_notification - should panic ... ok +test tests::revocation_test::test_cannot_revoke_already_revoked_notification - should panic ... ok +test tests::revocation_test::test_revoke_notification_by_creator ... ok +test tests::schema_version_test::test_is_version_supported_returns_false_for_zero ... ok +test tests::schema_version_test::test_is_version_supported_returns_false_for_future ... ok +test tests::revocation_test::test_revoke_by_unauthorized_user_fails - should panic ... ok +test tests::revocation_test::test_revoke_event_has_notification_category ... ok +test tests::schema_version_test::test_is_version_supported_returns_true_for_valid ... ok +test tests::schema_version_test::test_schema_version_default_is_zero ... ok +test tests::revocation_test::test_revoke_notification_emits_event ... ok +test tests::schema_version_test::test_set_schema_version_previous_version_tracked ... ok +test tests::revocation_test::test_revoke_event_has_high_priority ... ok +test tests::revocation_test::test_revoke_notification_by_admin ... ok +test tests::schema_version_test::test_set_schema_version_requires_admin - should panic ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_admin_operations ... ok +test tests::schema_version_test::test_set_schema_version_rejects_unsupported_version - should panic ... ok +test tests::schema_version_test::test_set_schema_version_stores_and_emits ... ok +test tests::schema_version_test::test_set_schema_version_rejects_zero - should panic ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_pause_flag ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_supported_tokens ... ok +test tests::revocation_test::test_revoked_notification_still_queryable ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_instance_storage_usage_fee ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_blocked_when_paused - should panic ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_update_members_single_write_consistency ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_event_topic_shape ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_is_group_member_uses_embedded_storage ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_deactivates_group ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_unknown_group_panics - should panic ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_rejects_already_inactive_group - should panic ... ok +test tests::storage_optimization_test::storage_optimization_tests::test_members_embedded_in_details ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_emits_event ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_event_data_contains_cancelled_at ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_by_member ... ok +test tests::template_registry_test::template_registry_tests::test_non_owner_cannot_update_template - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_multiple_templates_are_independent ... ok +test tests::template_registry_test::template_registry_tests::test_register_with_name_too_long_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_duplicate_registration_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_owner_can_update_template ... ok +test tests::template_registry_test::template_registry_tests::test_get_template_panics_for_nonexistent_id - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_register_emits_template_registered_event ... ok +test tests::template_registry_test::template_registry_tests::test_registered_template_can_be_retrieved ... ok +test tests::subscription_cancellation_test::test_cancel_subscription_rejects_non_member - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_register_with_empty_content_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_register_template_succeeds ... ok +test tests::template_registry_test::template_registry_tests::test_template_exists_returns_false_for_unknown_id ... ok +test tests::test_utils_test::test_create_zero_users ... ok +test tests::template_registry_test::template_registry_tests::test_update_nonexistent_template_panics - should panic ... ok +test tests::template_registry_test::template_registry_tests::test_template_exists_returns_true_for_registered_id ... ok +test tests::template_registry_test::template_registry_tests::test_update_emits_template_updated_event ... ok +test tests::version_test::test_version ... ok +test tests::test_utils_test::test_mock_token_helpers ... ok +test tests::test_utils_test::test_setup_test_env ... ok +test tests::test_utils_test::test_autoshare_helpers ... ok +test tests::fuzz_test::fuzz_notification_ttl_positive_always_schedules ... ok +test tests::fuzz_test::fuzz_member_percentages_sum_to_100_succeed ... ok + +test result: ok. 502 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.44s + diff --git a/contract/contracts/hello-world/src/autoshare_logic.rs b/contract/contracts/hello-world/src/autoshare_logic.rs index 4558c8b3..bfab2f8a 100644 --- a/contract/contracts/hello-world/src/autoshare_logic.rs +++ b/contract/contracts/hello-world/src/autoshare_logic.rs @@ -7,10 +7,6 @@ use crate::base::events::{ NotificationDelivered, NotificationExpired, NotificationExtended, NotificationLimitsConfigured, NotificationPriority, NotificationRecalled, NotificationRevoked, NotificationScheduled, OwnershipTransferInitiated, OwnershipTransferred, - ContractPaused, ContractUnpaused, GroupActivated, GroupDeactivated, NotificationAccessed, - NotificationAcknowledged, NotificationCategory, NotificationDelivered, NotificationExpired, - NotificationExtended, NotificationLimitsConfigured, NotificationPriority, NotificationRecalled, - NotificationRevoked, NotificationScheduled, OwnershipTransferInitiated, OwnershipTransferred, ScheduledNotificationCancelled, SchemaVersionSet, SubscriptionCancelled, Withdrawal, }; use crate::base::metadata_validation::{validate_metadata, NotificationMetadata}; @@ -314,7 +310,6 @@ pub fn add_group_member( // Add new member (embedded in AutoShareDetails — no separate GroupMembers key) details.members.push_back(GroupMember { address, - address: address.clone(), percentage, }); @@ -450,6 +445,10 @@ pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) -> R current_admin.require_auth(); require_admin(&env, ¤t_admin)?; + if get_paused_status(&env) { + return Err(Error::ContractPaused); + } + // Reject transfers to the zero address (Soroban does not have a literal // address(0), but callers must not pass the contract's own address as the // new owner, or an address that has never been set. The canonical "zero @@ -1338,6 +1337,39 @@ pub fn get_notification( load_notification(&env, ¬ification_id).ok_or(Error::NotFound) } +/// Returns the stored details for a scheduled notification, restricted to the +/// notification's creator or the contract admin. +/// +/// Unlike [`get_notification`], which is a permissionless read, this variant +/// rejects any authenticated caller outside the notification's permitted scope +/// with [`Error::Unauthorized`] and an [`AuthorizationFailure`] event, so +/// competing users cannot read one another's notification records. Unknown +/// notifications still yield [`Error::NotFound`]. +/// +/// # Errors +/// - `NotFound` if the notification is not tracked +/// - `Unauthorized` if `caller` is neither the creator nor the admin +pub fn get_notification_for( + env: Env, + notification_id: BytesN<32>, + caller: Address, +) -> Result { + caller.require_auth(); + + let notification = get_notification(env.clone(), notification_id)?; + + let admin = get_admin(env.clone()).ok(); + let is_creator = caller == notification.creator; + let is_admin = admin.as_ref().map_or(false, |a| caller == *a); + + if !is_creator && !is_admin { + publish_authorization_failure(&env, &caller, "get_notification_for"); + return Err(Error::Unauthorized); + } + + Ok(notification) +} + /// Returns whether a tracked notification has expired. Errors with /// [`Error::NotFound`] if the notification is not tracked. pub fn is_notification_expired(env: Env, notification_id: BytesN<32>) -> Result { @@ -1872,6 +1904,25 @@ pub fn revoke_notification( // Audit Logging // ============================================================================ +/// Loads a tracked notification for an audit write, rejecting unknown, revoked, +/// and expired notifications so audit records always reference a live record. +fn load_auditable_notification( + env: &Env, + notification_id: &BytesN<32>, +) -> Result { + let notification = load_notification(env, notification_id).ok_or(Error::NotFound)?; + + if is_revoked(¬ification) { + return Err(Error::NotificationRevoked); + } + + if is_expired(env, ¬ification) { + return Err(Error::NotificationExpired); + } + + Ok(notification) +} + /// Appends an immutable [`AuditRecord`] to the on-chain audit log and emits an /// [`AuditRecordAppended`] event. The sequence number is auto-incremented. fn append_audit_record( @@ -1948,7 +1999,8 @@ pub fn get_audit_records_for_notification( /// Records a delivery attempt for a notification in the audit log. /// /// This is a permissionless write so any authorised service (an off-chain -/// relay, a keeper) can record that it attempted delivery. +/// relay, a keeper) can record that it attempted delivery. Unknown, revoked, +/// and expired notifications are rejected. pub fn record_delivery_attempt( env: Env, notification_id: BytesN<32>, @@ -1960,11 +2012,15 @@ pub fn record_delivery_attempt( return Err(Error::ContractPaused); } + load_auditable_notification(&env, ¬ification_id)?; + append_audit_record(&env, notification_id, AuditAction::DeliveryAttempt, actor); Ok(()) } /// Records a delivery failure for a notification in the audit log. +/// +/// Unknown, revoked, and expired notifications are rejected. pub fn record_delivery_failure( env: Env, notification_id: BytesN<32>, @@ -1976,11 +2032,15 @@ pub fn record_delivery_failure( return Err(Error::ContractPaused); } + load_auditable_notification(&env, ¬ification_id)?; + append_audit_record(&env, notification_id, AuditAction::DeliveryFailed, actor); Ok(()) } /// Records that the recipient acknowledged a notification. +/// +/// Unknown, revoked, and expired notifications are rejected. pub fn record_acknowledgment( env: Env, notification_id: BytesN<32>, @@ -1992,6 +2052,8 @@ pub fn record_acknowledgment( return Err(Error::ContractPaused); } + load_auditable_notification(&env, ¬ification_id)?; + append_audit_record(&env, notification_id, AuditAction::Acknowledged, actor); Ok(()) } @@ -2004,6 +2066,38 @@ pub fn is_notification_revoked(env: Env, notification_id: BytesN<32>) -> Result< Ok(is_revoked(¬ification)) } +/// Returns whether a scheduled notification has been revoked, restricted to the +/// notification's creator or the contract admin. +/// +/// The permissionless [`is_notification_revoked`] read is only appropriate when +/// the caller has already been authorized elsewhere; this variant performs the +/// same ownership check as [`get_notification_for`] so revocation status cannot +/// be polled for another party's notification. +/// +/// # Errors +/// - `NotFound` if the notification is not tracked +/// - `Unauthorized` if `caller` is neither the creator nor the admin +pub fn is_notification_revoked_for( + env: Env, + notification_id: BytesN<32>, + caller: Address, +) -> Result { + caller.require_auth(); + + let notification = get_notification(env.clone(), notification_id)?; + + let admin = get_admin(env.clone()).ok(); + let is_creator = caller == notification.creator; + let is_admin = admin.as_ref().map_or(false, |a| caller == *a); + + if !is_creator && !is_admin { + publish_authorization_failure(&env, &caller, "is_notification_revoked_for"); + return Err(Error::Unauthorized); + } + + Ok(is_revoked(¬ification)) +} + /// Acknowledges multiple scheduled notifications in a single batch. /// /// Only the creator of the notification can acknowledge it. The notification diff --git a/contract/contracts/hello-world/src/base/events.rs b/contract/contracts/hello-world/src/base/events.rs index 863b3ba9..21c2065c 100644 --- a/contract/contracts/hello-world/src/base/events.rs +++ b/contract/contracts/hello-world/src/base/events.rs @@ -130,7 +130,17 @@ pub struct CategoryRegistered { } /// Emitted when the contract is paused by the admin. -#[contractevent] +#[contractevent(data_format = "single-value")] +#[derive(Clone)] +pub struct ContractPaused { + #[topic] + pub admin: Address, + #[topic] + pub category: NotificationCategory, + #[topic] + pub priority: NotificationPriority, +} + /// Emitted when a recipient updates a delivery channel preference. /// /// Off-chain consumers can filter on `recipient` and inspect `channel` / @@ -153,18 +163,6 @@ pub struct ChannelPreferenceUpdated { pub updated_at: u64, } -/// Emitted when an AutoShare group is deactivated by its creator. -#[contractevent(data_format = "single-value")] -#[derive(Clone)] -pub struct ContractPaused { - #[topic] - pub admin: Address, - #[topic] - pub category: NotificationCategory, - #[topic] - pub priority: NotificationPriority, -} - /// Emitted when the contract is unpaused by the admin. #[contractevent] #[derive(Clone)] diff --git a/contract/contracts/hello-world/src/base/idempotency.rs b/contract/contracts/hello-world/src/base/idempotency.rs new file mode 100644 index 00000000..9f15a018 --- /dev/null +++ b/contract/contracts/hello-world/src/base/idempotency.rs @@ -0,0 +1,54 @@ +/// Idempotency records for caller-supplied notification delivery requests. +/// +/// A delivery request is uniquely identified by a caller-supplied +/// `idempotency_key`. The first successful submission persists an +/// [`IdempotencyRecord`]; any later submission carrying the same key observes +/// the recorded outcome instead of creating a duplicate delivery. +use soroban_sdk::{contracttype, Address, BytesN, String}; + +// ============================================================================ +// Types +// ============================================================================ + +/// Persisted idempotency record for a notification delivery request. +/// +/// A delivery request is uniquely identified by the caller-supplied +/// `idempotency_key`. The first successful submission stores this record; any +/// subsequent submission with the same key returns the recorded outcome +/// instead of creating a duplicate delivery. +/// +/// # Field ordering (storage optimization — issue #371) +/// +/// Fixed-width fields are grouped before the variable-length `result` string: +/// +/// 1. Identity — `key` (BytesN<32>), `notification_id` (BytesN<32>) +/// 2. Ownership — `submitter` (Address) +/// 3. Scalars — `created_at` (u64), `delivered` (bool) +/// 4. Variable — `result` (String) +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IdempotencyRecord { + /// Caller-supplied idempotency key uniquely identifying the request. + pub key: BytesN<32>, + /// Notification identifier the delivery request targeted. + pub notification_id: BytesN<32>, + /// Address that submitted the original delivery request. + pub submitter: Address, + /// Ledger timestamp (seconds) when the record was first persisted. + pub created_at: u64, + /// Whether the original delivery request completed successfully. + pub delivered: bool, + /// Opaque result payload returned to duplicate callers verbatim. + pub result: String, +} + +// ============================================================================ +// Storage keys +// ============================================================================ + +#[contracttype] +#[derive(Clone)] +pub enum IdempotencyKey { + /// Delivery idempotency record keyed by the caller-supplied key. + Record(BytesN<32>), +} diff --git a/contract/contracts/hello-world/src/base/reputation.rs b/contract/contracts/hello-world/src/base/reputation.rs index eabc71c1..9dad2bd2 100644 --- a/contract/contracts/hello-world/src/base/reputation.rs +++ b/contract/contracts/hello-world/src/base/reputation.rs @@ -58,8 +58,11 @@ pub fn calculate_reputation_score(successful: u32, failed: u32) -> i64 { return INITIAL_REPUTATION_SCORE; } - let success_rate = (successful as f64 / total as f64) * 100.0; - let score = (success_rate / 2.0) as i64 + 25; + // Weight the score by the squared success rate so a perfect record reaches + // the maximum, a balanced record lands well below it, and a record with no + // successful deliveries bottoms out at the minimum. + let success_rate = successful as f64 / total as f64; + let score = (success_rate * success_rate * MAX_REPUTATION_SCORE as f64) as i64; // Clamp score to valid range if score > MAX_REPUTATION_SCORE { diff --git a/contract/contracts/hello-world/src/base/types.rs b/contract/contracts/hello-world/src/base/types.rs index 65326fe4..3590a373 100644 --- a/contract/contracts/hello-world/src/base/types.rs +++ b/contract/contracts/hello-world/src/base/types.rs @@ -243,39 +243,3 @@ pub struct NotificationLimits { /// Maximum number of notifications in a batch operation pub max_batch_size: u32, } - -// ============================================================================ -// Notification Delivery Idempotency -// ============================================================================ - -/// Persisted idempotency record for a notification delivery request. -/// -/// A delivery request is uniquely identified by the caller-supplied -/// `idempotency_key`. The first successful submission stores this record; any -/// subsequent submission with the same key returns the recorded outcome -/// instead of creating a duplicate delivery. -/// -/// # Field ordering (storage optimization — issue #371) -/// -/// Fixed-width fields are grouped before the variable-length `result` string: -/// -/// 1. Identity — `key` (BytesN<32>), `notification_id` (BytesN<32>) -/// 2. Ownership — `submitter` (Address) -/// 3. Scalars — `created_at` (u64), `delivered` (bool) -/// 4. Variable — `result` (String) -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct IdempotencyRecord { - /// Caller-supplied idempotency key uniquely identifying the request. - pub key: BytesN<32>, - /// Notification identifier the delivery request targeted. - pub notification_id: BytesN<32>, - /// Address that submitted the original delivery request. - pub submitter: Address, - /// Ledger timestamp (seconds) when the record was first persisted. - pub created_at: u64, - /// Whether the original delivery request completed successfully. - pub delivered: bool, - /// Opaque result payload returned to duplicate callers verbatim. - pub result: String, -} diff --git a/contract/contracts/hello-world/src/idempotency_logic.rs b/contract/contracts/hello-world/src/idempotency_logic.rs new file mode 100644 index 00000000..05336e95 --- /dev/null +++ b/contract/contracts/hello-world/src/idempotency_logic.rs @@ -0,0 +1,61 @@ +/// Idempotent notification delivery — issue #371 +/// +/// Records delivery requests against a caller-supplied idempotency key so that +/// retried or concurrently submitted requests do not create duplicate +/// deliveries. The first caller to commit a given key wins; every later caller +/// with the same key observes the recorded outcome. +use crate::base::errors::Error; +use crate::base::idempotency::{IdempotencyKey, IdempotencyRecord}; +use soroban_sdk::{Address, BytesN, Env, String}; + +/// Records a notification delivery attempt idempotently. +/// +/// If `idempotency_key` has already been used, the identifier stored by the +/// first caller is returned and no new delivery is created. Otherwise a record +/// is persisted and `notification_id` is returned. +pub fn deliver_notification_idempotent( + env: Env, + idempotency_key: BytesN<32>, + notification_id: BytesN<32>, + recipient: Address, +) -> Result, Error> { + recipient.require_auth(); + + let key = IdempotencyKey::Record(idempotency_key.clone()); + + // A duplicate request replays the first caller's outcome instead of + // recording a second delivery. + if let Some(existing) = env + .storage() + .persistent() + .get::(&key) + { + return Ok(existing.notification_id); + } + + let record = IdempotencyRecord { + key: idempotency_key, + notification_id: notification_id.clone(), + submitter: recipient, + created_at: env.ledger().timestamp(), + delivered: true, + result: String::from_str(&env, "delivered"), + }; + env.storage().persistent().set(&key, &record); + + Ok(notification_id) +} + +/// Returns the stored idempotency record for `idempotency_key`, if any. +pub fn get_idempotency_record(env: Env, idempotency_key: BytesN<32>) -> Option { + env.storage() + .persistent() + .get(&IdempotencyKey::Record(idempotency_key)) +} + +/// Returns whether an idempotency key has already been used. +pub fn is_idempotency_key_used(env: Env, idempotency_key: BytesN<32>) -> bool { + env.storage() + .persistent() + .has(&IdempotencyKey::Record(idempotency_key)) +} diff --git a/contract/contracts/hello-world/src/lib.rs b/contract/contracts/hello-world/src/lib.rs index f5386bed..3f09c33a 100644 --- a/contract/contracts/hello-world/src/lib.rs +++ b/contract/contracts/hello-world/src/lib.rs @@ -1,4 +1,10 @@ #![no_std] + +// The Soroban runtime is `no_std`, but the test harness runs on the host and +// relies on `std` (panics, formatting, allocations). Link it back in for tests. +#[cfg(test)] +extern crate std; + use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Vec}; // 1. Declare the foundational modules (Requirement: Modular Structure) @@ -50,13 +56,13 @@ impl AutoShareContract { } /// Pauses the contract. Only admin can call. - pub fn pause(env: Env, admin: Address) { - autoshare_logic::pause(env, admin).unwrap(); + pub fn pause(env: Env, admin: Address) -> Result<(), base::errors::Error> { + autoshare_logic::pause(env, admin) } /// Unpauses the contract. Only admin can call. - pub fn unpause(env: Env, admin: Address) { - autoshare_logic::unpause(env, admin).unwrap(); + pub fn unpause(env: Env, admin: Address) -> Result<(), base::errors::Error> { + autoshare_logic::unpause(env, admin) } /// Returns the current pause status. @@ -98,9 +104,8 @@ impl AutoShareContract { creator: Address, usage_count: u32, payment_token: Address, - ) { + ) -> Result<(), base::errors::Error> { autoshare_logic::create_autoshare(env, id, name, creator, usage_count, payment_token) - .unwrap(); } /// Update members of an existing AutoShare plan. @@ -110,8 +115,8 @@ impl AutoShareContract { id: BytesN<32>, caller: Address, new_members: Vec, - ) { - autoshare_logic::update_members(env, id, caller, new_members).unwrap(); + ) -> Result<(), base::errors::Error> { + autoshare_logic::update_members(env, id, caller, new_members) } /// Retrieves an existing AutoShare plan. @@ -153,15 +158,19 @@ impl AutoShareContract { caller: Address, address: Address, percentage: u32, - ) { - autoshare_logic::add_group_member(env, id, caller, address, percentage).unwrap(); + ) -> Result<(), base::errors::Error> { + autoshare_logic::add_group_member(env, id, caller, address, percentage) } /// Deactivates a channel (group). Callable by the creator or the contract /// admin. Historical membership and payment history remain visible; new /// subscriptions (top-ups) are blocked until reactivated. - pub fn deactivate_group(env: Env, id: BytesN<32>, caller: Address) { - autoshare_logic::deactivate_group(env, id, caller).unwrap(); + pub fn deactivate_group( + env: Env, + id: BytesN<32>, + caller: Address, + ) -> Result<(), base::errors::Error> { + autoshare_logic::deactivate_group(env, id, caller) } /// Activates a group. Only the creator can activate. @@ -181,8 +190,12 @@ impl AutoShareContract { /// Transfers admin rights to a new address. Only current admin can call. /// Rejects transfers where new_admin == current_admin. - pub fn transfer_admin(env: Env, current_admin: Address, new_admin: Address) { - autoshare_logic::transfer_admin(env, current_admin, new_admin).unwrap(); + pub fn transfer_admin( + env: Env, + current_admin: Address, + new_admin: Address, + ) -> Result<(), base::errors::Error> { + autoshare_logic::transfer_admin(env, current_admin, new_admin) } // ============================================================================ @@ -284,9 +297,8 @@ impl AutoShareContract { additional_usages: u32, payment_token: Address, payer: Address, - ) { + ) -> Result<(), base::errors::Error> { autoshare_logic::topup_subscription(env, id, additional_usages, payment_token, payer) - .unwrap(); } /// Cancels an active notification subscription for a group. @@ -327,9 +339,8 @@ impl AutoShareContract { autoshare_logic::get_total_usages_paid(env, id).unwrap() } - /// Reduces the usage count by 1. - pub fn reduce_usage(env: Env, id: BytesN<32>) { - let caller = env.current_contract_address(); + /// Reduces the usage count by 1. Only the group's creator may call this. + pub fn reduce_usage(env: Env, id: BytesN<32>, caller: Address) { autoshare_logic::reduce_usage(env, id, caller).unwrap(); } @@ -455,6 +466,16 @@ impl AutoShareContract { autoshare_logic::get_notification(env, notification_id).unwrap() } + /// Returns the stored details for a scheduled notification, restricted to + /// the notification's creator or the contract admin. + pub fn get_notification_for( + env: Env, + notification_id: BytesN<32>, + caller: Address, + ) -> base::types::ScheduledNotification { + autoshare_logic::get_notification_for(env, notification_id, caller).unwrap() + } + /// Returns whether a scheduled notification has expired. pub fn is_notification_expired(env: Env, notification_id: BytesN<32>) -> bool { autoshare_logic::is_notification_expired(env, notification_id).unwrap() @@ -563,6 +584,16 @@ impl AutoShareContract { autoshare_logic::is_notification_revoked(env, notification_id).unwrap() } + /// Returns whether a scheduled notification has been revoked, restricted to + /// the notification's creator or the contract admin. + pub fn is_notification_revoked_for( + env: Env, + notification_id: BytesN<32>, + caller: Address, + ) -> bool { + autoshare_logic::is_notification_revoked_for(env, notification_id, caller).unwrap() + } + /// Acknowledges multiple scheduled notifications in a single batch. pub fn acknowledge_notifications(env: Env, caller: Address, notification_ids: Vec>) { autoshare_logic::acknowledge_notifications(env, caller, notification_ids).unwrap(); @@ -822,64 +853,29 @@ impl AutoShareContract { pub fn is_idempotency_key_used(env: Env, idempotency_key: BytesN<32>) -> bool { idempotency_logic::is_idempotency_key_used(env, idempotency_key) } -} - -#[cfg(test)] -#[path = "tests/test_utils.rs"] -pub mod test_utils; - -#[cfg(test)] -mod tests { - // Preexisting broken suites temporarily excluded so new feature tests can compile. - // mod test_utils_test; - // mod storage_optimization_test; - // mod preferences_test; - // mod autoshare_test; - // mod pause_test; - // mod mock_token_test; - mod version_test; - // mod notification_test; - // mod expiration_test; - // mod revocation_test; - // mod ownership_transfer_test; - // mod notification_validation_test; - // mod category_registry_test; - // mod batch_notification_test; - // mod audit_log_test; - // mod payload_validation_test; - // mod batch_ack_test; - // mod fuzz_test; - mod schema_version_test; - // mod access_log_test; - // mod subscription_cancellation_test; - mod channel_metadata_test; - mod notification_version_test; - mod metadata_validation_test; - mod archive_notification_test; - mod idempotency_test; // ============================================================================ - // Notification Channel Subscriptions + // Notification Channels // ============================================================================ - /// Creates a notification channel and permanently stores the creator address. + /// Creates a new notification channel owned by `creator`. pub fn create_channel(env: Env, id: BytesN<32>, name: String, creator: Address) { channel_logic::create_channel(env, id, name, creator).unwrap(); } - /// Returns full channel metadata (creator, name, subscriber_count, etc.). + /// Returns channel metadata including creator and subscriber count. pub fn get_channel(env: Env, id: BytesN<32>) -> base::channel::NotificationChannel { channel_logic::get_channel(env, id).unwrap() } /// Returns the wallet address that originally created the channel. - pub fn get_channel_creator(env: Env, id: BytesN<32>) -> Address { - channel_logic::get_channel_creator(env, id).unwrap() + pub fn get_channel_creator(env: Env, id: BytesN<32>) -> Result { + channel_logic::get_channel_creator(env, id) } /// Read-only view of the active subscriber count for a channel. - pub fn get_subscriber_count(env: Env, id: BytesN<32>) -> u32 { - channel_logic::get_subscriber_count(env, id).unwrap() + pub fn get_subscriber_count(env: Env, id: BytesN<32>) -> Result { + channel_logic::get_subscriber_count(env, id) } /// Returns whether `subscriber` is currently subscribed to the channel. @@ -887,120 +883,66 @@ mod tests { channel_logic::is_channel_subscriber(env, id, subscriber) } - /// Subscribe to a single notification channel. - pub fn subscribe(env: Env, channel_id: BytesN<32>, subscriber: Address) { - channel_logic::subscribe(env, channel_id, subscriber).unwrap(); + /// Subscribes an address to a channel. + pub fn subscribe( + env: Env, + channel_id: BytesN<32>, + subscriber: Address, + ) -> Result<(), base::errors::Error> { + channel_logic::subscribe(env, channel_id, subscriber) } - /// Unsubscribe from a notification channel. + /// Unsubscribes an address from a channel. pub fn unsubscribe(env: Env, channel_id: BytesN<32>, subscriber: Address) { channel_logic::unsubscribe(env, channel_id, subscriber).unwrap(); } - /// Subscribe to multiple channels in one transaction. - /// - /// Individual failures are skipped without corrupting successful subscriptions. - /// See `docs/BATCH_SUBSCRIBE_GAS.md` for gas documentation. + /// Subscribes an address to multiple channels in a single transaction. pub fn batch_subscribe( env: Env, channel_ids: Vec>, subscriber: Address, - ) -> base::channel::BatchSubscribeResult { - channel_logic::batch_subscribe(env, channel_ids, subscriber).unwrap() + ) -> Result { + channel_logic::batch_subscribe(env, channel_ids, subscriber) } } #[cfg(test)] -pub mod test_utils { - #[path = "../tests/test_utils.rs"] - mod inner; - pub use inner::*; -} +#[path = "tests/test_utils.rs"] +pub mod test_utils; #[cfg(test)] mod tests { - #[path = "tests/test_utils_test.rs"] - mod test_utils_test; - #[path = "tests/storage_optimization_test.rs"] - mod storage_optimization_test; - #[path = "tests/preferences_test.rs"] - mod preferences_test; - #[path = "tests/autoshare_test.rs"] + mod access_control_test; + mod access_log_test; + mod archive_notification_test; + mod audit_log_test; mod autoshare_test; - #[path = "tests/pause_test.rs"] - mod pause_test; - #[path = "tests/mock_token_test.rs"] + mod batch_ack_test; + mod batch_event_test; + mod batch_notification_test; + mod category_registry_test; + mod channel_metadata_test; + mod channel_subscription_test; + mod event_regression_test; + mod expiration_test; + mod extended_coverage_test; + mod fuzz_test; + mod metadata_validation_test; mod mock_token_test; - #[path = "tests/version_test.rs"] - mod version_test; - #[path = "tests/notification_test.rs"] + mod notification_lifetime_test; mod notification_test; - #[path = "tests/expiration_test.rs"] - mod expiration_test; - #[path = "tests/revocation_test.rs"] - mod revocation_test; - #[path = "tests/ownership_transfer_test.rs"] - mod ownership_transfer_test; - #[path = "tests/notification_validation_test.rs"] mod notification_validation_test; - #[path = "tests/category_registry_test.rs"] - mod category_registry_test; - #[path = "tests/batch_notification_test.rs"] - mod batch_notification_test; - #[path = "tests/audit_log_test.rs"] - mod audit_log_test; - #[path = "tests/payload_validation_test.rs"] + mod notification_version_test; + mod ownership_transfer_test; + mod pause_test; mod payload_validation_test; - #[path = "tests/batch_ack_test.rs"] - mod batch_ack_test; - #[path = "tests/fuzz_test.rs"] - mod fuzz_test; - #[path = "tests/schema_version_test.rs"] + mod preferences_test; + mod revocation_test; mod schema_version_test; - #[path = "tests/access_log_test.rs"] - mod access_log_test; - #[path = "tests/subscription_cancellation_test.rs"] + mod storage_optimization_test; mod subscription_cancellation_test; - #[path = "tests/extended_coverage_test.rs"] - mod extended_coverage_test; - - #[path = "tests/notification_validation_test.rs"] - mod notification_validation_test; - - #[path = "tests/category_registry_test.rs"] - mod category_registry_test; - - #[path = "tests/batch_notification_test.rs"] - mod batch_notification_test; - - #[path = "tests/batch_event_test.rs"] - mod batch_event_test; - - #[path = "tests/audit_log_test.rs"] - mod audit_log_test; - - #[path = "tests/payload_validation_test.rs"] - mod payload_validation_test; - - #[path = "tests/batch_ack_test.rs"] - mod batch_ack_test; - - #[path = "tests/fuzz_test.rs"] - mod fuzz_test; - - #[path = "tests/schema_version_test.rs"] - mod schema_version_test; - - #[path = "tests/access_log_test.rs"] - mod access_log_test; - - #[path = "../tests/template_registry_test.rs"] mod template_registry_test; - #[path = "tests/subscription_cancellation_test.rs"] - mod subscription_cancellation_test; - - #[path = "../tests/channel_subscription_test.rs"] - mod channel_subscription_test; - #[path = "tests/notification_lifetime_test.rs"] - mod notification_lifetime_test; + mod test_utils_test; + mod version_test; } diff --git a/contract/contracts/hello-world/src/mock_token.rs b/contract/contracts/hello-world/src/mock_token.rs index 9c41d3c2..156f7e73 100644 --- a/contract/contracts/hello-world/src/mock_token.rs +++ b/contract/contracts/hello-world/src/mock_token.rs @@ -101,230 +101,3 @@ impl MockToken { .unwrap_or(0) } } -/// Notification channel creation, subscription, and subscriber-count views. -use crate::base::channel::{ - is_subscribed, load_channel, load_subscribers, push_all_channel_id, save_channel, - save_subscribers, set_subscribed, BatchSubscribeCompleted, BatchSubscribeResult, - ChannelCreated, ChannelSubscribed, ChannelUnsubscribed, NotificationChannel, - MAX_BATCH_SUBSCRIBE, MAX_CHANNEL_NAME_LENGTH, -}; -use crate::base::errors::Error; -use crate::base::events::{NotificationCategory, NotificationPriority}; -use soroban_sdk::{Address, BytesN, Env, String, Vec}; - -fn require_not_paused(env: &Env) -> Result<(), Error> { - if crate::autoshare_logic::get_paused_status(env) { - return Err(Error::ContractPaused); - } - Ok(()) -} - -/// Creates a new notification channel owned by `creator`. -/// -/// Stores the creator address permanently so it can be queried later. -/// Emits a [`ChannelCreated`] event. -pub fn create_channel( - env: Env, - id: BytesN<32>, - name: String, - creator: Address, -) -> Result<(), Error> { - creator.require_auth(); - require_not_paused(&env)?; - - if name.is_empty() { - return Err(Error::InvalidInput); - } - if name.len() > MAX_CHANNEL_NAME_LENGTH { - return Err(Error::NameTooLong); - } - - if load_channel(&env, &id).is_some() { - return Err(Error::AlreadyExists); - } - - let channel = NotificationChannel { - id: id.clone(), - creator: creator.clone(), - name, - subscriber_count: 0, - is_active: true, - created_at: env.ledger().timestamp(), - }; - save_channel(&env, &channel); - push_all_channel_id(&env, &id); - - ChannelCreated { - creator, - category: NotificationCategory::Notification, - priority: NotificationPriority::Medium, - channel_id: id, - } - .publish(&env); - - Ok(()) -} - -/// Returns channel metadata including creator and subscriber count. -pub fn get_channel(env: Env, id: BytesN<32>) -> Result { - load_channel(&env, &id).ok_or(Error::NotFound) -} - -/// Returns the wallet address that originally created the channel. -pub fn get_channel_creator(env: Env, id: BytesN<32>) -> Result { - Ok(load_channel(&env, &id).ok_or(Error::NotFound)?.creator) -} - -/// Read-only view of the active subscriber count for a channel. -pub fn get_subscriber_count(env: Env, id: BytesN<32>) -> Result { - Ok(load_channel(&env, &id) - .ok_or(Error::NotFound)? - .subscriber_count) -} - -/// Returns whether `subscriber` is currently subscribed to the channel. -pub fn is_channel_subscriber(env: Env, id: BytesN<32>, subscriber: Address) -> bool { - is_subscribed(&env, &id, &subscriber) -} - -/// Subscribe a single address to a channel. -pub fn subscribe(env: Env, channel_id: BytesN<32>, subscriber: Address) -> Result<(), Error> { - subscriber.require_auth(); - require_not_paused(&env)?; - subscribe_one(&env, &channel_id, &subscriber)?; - Ok(()) -} - -/// Unsubscribe from a channel. No-ops the count if not currently subscribed. -pub fn unsubscribe(env: Env, channel_id: BytesN<32>, subscriber: Address) -> Result<(), Error> { - subscriber.require_auth(); - require_not_paused(&env)?; - - let mut channel = load_channel(&env, &channel_id).ok_or(Error::NotFound)?; - - if !is_subscribed(&env, &channel_id, &subscriber) { - return Err(Error::NotFound); - } - - set_subscribed(&env, &channel_id, &subscriber, false); - - let mut subscribers = load_subscribers(&env, &channel_id); - let mut next = Vec::new(&env); - for addr in subscribers.iter() { - if addr != subscriber { - next.push_back(addr); - } - } - save_subscribers(&env, &channel_id, &next); - - channel.subscriber_count = channel.subscriber_count.saturating_sub(1); - save_channel(&env, &channel); - - ChannelUnsubscribed { - channel_id, - subscriber, - category: NotificationCategory::Notification, - subscriber_count: channel.subscriber_count, - } - .publish(&env); - - Ok(()) -} - -/// Subscribe to multiple channels in a single transaction. -/// -/// Failed individual subscriptions (missing channel, inactive, already -/// subscribed) are skipped and do not roll back successful ones — state for -/// failed entries is left unchanged. Structural errors (empty batch, too large, -/// paused contract) abort the whole call before any mutation. -/// -/// # Gas notes -/// See `docs/BATCH_SUBSCRIBE_GAS.md`. Batching N subscriptions into one -/// transaction avoids N separate transaction base fees and repeated auth -/// overhead. Per-channel storage writes still scale linearly with N. -pub fn batch_subscribe( - env: Env, - channel_ids: Vec>, - subscriber: Address, -) -> Result { - subscriber.require_auth(); - require_not_paused(&env)?; - - let count = channel_ids.len(); - if count == 0 { - return Err(Error::InvalidInput); - } - if count > MAX_BATCH_SUBSCRIBE { - return Err(Error::BatchTooLarge); - } - - let mut succeeded = 0u32; - let mut failed = 0u32; - let mut subscribed_ids = Vec::new(&env); - - for i in 0..count { - let channel_id = channel_ids.get(i).unwrap(); - match subscribe_one(&env, &channel_id, &subscriber) { - Ok(()) => { - succeeded += 1; - subscribed_ids.push_back(channel_id); - } - Err(_) => { - failed += 1; - } - } - } - - BatchSubscribeCompleted { - subscriber: subscriber.clone(), - category: NotificationCategory::Notification, - priority: NotificationPriority::Medium, - succeeded, - failed, - } - .publish(&env); - - Ok(BatchSubscribeResult { - succeeded, - failed, - subscribed_ids, - }) -} - -fn subscribe_one( - env: &Env, - channel_id: &BytesN<32>, - subscriber: &Address, -) -> Result<(), Error> { - let mut channel = load_channel(env, channel_id).ok_or(Error::NotFound)?; - - if !channel.is_active { - return Err(Error::GroupInactive); - } - - if is_subscribed(env, channel_id, subscriber) { - return Err(Error::AlreadyExists); - } - - set_subscribed(env, channel_id, subscriber, true); - - let mut subscribers = load_subscribers(env, channel_id); - subscribers.push_back(subscriber.clone()); - save_subscribers(env, channel_id, &subscribers); - - channel.subscriber_count = channel - .subscriber_count - .checked_add(1) - .ok_or(Error::InvalidInput)?; - save_channel(env, &channel); - - ChannelSubscribed { - channel_id: channel_id.clone(), - subscriber: subscriber.clone(), - category: NotificationCategory::Notification, - subscriber_count: channel.subscriber_count, - } - .publish(env); - - Ok(()) -} diff --git a/contract/contracts/hello-world/src/template_registry_logic.rs b/contract/contracts/hello-world/src/template_registry_logic.rs index 610ce0fa..2d8d3b97 100644 --- a/contract/contracts/hello-world/src/template_registry_logic.rs +++ b/contract/contracts/hello-world/src/template_registry_logic.rs @@ -215,230 +215,3 @@ fn publish_authorization_failure(env: &Env, caller: &Address, action: &str) { } .publish(env); } -/// Notification channel creation, subscription, and subscriber-count views. -use crate::base::channel::{ - is_subscribed, load_channel, load_subscribers, push_all_channel_id, save_channel, - save_subscribers, set_subscribed, BatchSubscribeCompleted, BatchSubscribeResult, - ChannelCreated, ChannelSubscribed, ChannelUnsubscribed, NotificationChannel, - MAX_BATCH_SUBSCRIBE, MAX_CHANNEL_NAME_LENGTH, -}; -use crate::base::errors::Error; -use crate::base::events::{NotificationCategory, NotificationPriority}; -use soroban_sdk::{Address, BytesN, Env, String, Vec}; - -fn require_not_paused(env: &Env) -> Result<(), Error> { - if crate::autoshare_logic::get_paused_status(env) { - return Err(Error::ContractPaused); - } - Ok(()) -} - -/// Creates a new notification channel owned by `creator`. -/// -/// Stores the creator address permanently so it can be queried later. -/// Emits a [`ChannelCreated`] event. -pub fn create_channel( - env: Env, - id: BytesN<32>, - name: String, - creator: Address, -) -> Result<(), Error> { - creator.require_auth(); - require_not_paused(&env)?; - - if name.is_empty() { - return Err(Error::InvalidInput); - } - if name.len() > MAX_CHANNEL_NAME_LENGTH { - return Err(Error::NameTooLong); - } - - if load_channel(&env, &id).is_some() { - return Err(Error::AlreadyExists); - } - - let channel = NotificationChannel { - id: id.clone(), - creator: creator.clone(), - name, - subscriber_count: 0, - is_active: true, - created_at: env.ledger().timestamp(), - }; - save_channel(&env, &channel); - push_all_channel_id(&env, &id); - - ChannelCreated { - creator, - category: NotificationCategory::Notification, - priority: NotificationPriority::Medium, - channel_id: id, - } - .publish(&env); - - Ok(()) -} - -/// Returns channel metadata including creator and subscriber count. -pub fn get_channel(env: Env, id: BytesN<32>) -> Result { - load_channel(&env, &id).ok_or(Error::NotFound) -} - -/// Returns the wallet address that originally created the channel. -pub fn get_channel_creator(env: Env, id: BytesN<32>) -> Result { - Ok(load_channel(&env, &id).ok_or(Error::NotFound)?.creator) -} - -/// Read-only view of the active subscriber count for a channel. -pub fn get_subscriber_count(env: Env, id: BytesN<32>) -> Result { - Ok(load_channel(&env, &id) - .ok_or(Error::NotFound)? - .subscriber_count) -} - -/// Returns whether `subscriber` is currently subscribed to the channel. -pub fn is_channel_subscriber(env: Env, id: BytesN<32>, subscriber: Address) -> bool { - is_subscribed(&env, &id, &subscriber) -} - -/// Subscribe a single address to a channel. -pub fn subscribe(env: Env, channel_id: BytesN<32>, subscriber: Address) -> Result<(), Error> { - subscriber.require_auth(); - require_not_paused(&env)?; - subscribe_one(&env, &channel_id, &subscriber)?; - Ok(()) -} - -/// Unsubscribe from a channel. No-ops the count if not currently subscribed. -pub fn unsubscribe(env: Env, channel_id: BytesN<32>, subscriber: Address) -> Result<(), Error> { - subscriber.require_auth(); - require_not_paused(&env)?; - - let mut channel = load_channel(&env, &channel_id).ok_or(Error::NotFound)?; - - if !is_subscribed(&env, &channel_id, &subscriber) { - return Err(Error::NotFound); - } - - set_subscribed(&env, &channel_id, &subscriber, false); - - let mut subscribers = load_subscribers(&env, &channel_id); - let mut next = Vec::new(&env); - for addr in subscribers.iter() { - if addr != subscriber { - next.push_back(addr); - } - } - save_subscribers(&env, &channel_id, &next); - - channel.subscriber_count = channel.subscriber_count.saturating_sub(1); - save_channel(&env, &channel); - - ChannelUnsubscribed { - channel_id, - subscriber, - category: NotificationCategory::Notification, - subscriber_count: channel.subscriber_count, - } - .publish(&env); - - Ok(()) -} - -/// Subscribe to multiple channels in a single transaction. -/// -/// Failed individual subscriptions (missing channel, inactive, already -/// subscribed) are skipped and do not roll back successful ones — state for -/// failed entries is left unchanged. Structural errors (empty batch, too large, -/// paused contract) abort the whole call before any mutation. -/// -/// # Gas notes -/// See `docs/BATCH_SUBSCRIBE_GAS.md`. Batching N subscriptions into one -/// transaction avoids N separate transaction base fees and repeated auth -/// overhead. Per-channel storage writes still scale linearly with N. -pub fn batch_subscribe( - env: Env, - channel_ids: Vec>, - subscriber: Address, -) -> Result { - subscriber.require_auth(); - require_not_paused(&env)?; - - let count = channel_ids.len(); - if count == 0 { - return Err(Error::InvalidInput); - } - if count > MAX_BATCH_SUBSCRIBE { - return Err(Error::BatchTooLarge); - } - - let mut succeeded = 0u32; - let mut failed = 0u32; - let mut subscribed_ids = Vec::new(&env); - - for i in 0..count { - let channel_id = channel_ids.get(i).unwrap(); - match subscribe_one(&env, &channel_id, &subscriber) { - Ok(()) => { - succeeded += 1; - subscribed_ids.push_back(channel_id); - } - Err(_) => { - failed += 1; - } - } - } - - BatchSubscribeCompleted { - subscriber: subscriber.clone(), - category: NotificationCategory::Notification, - priority: NotificationPriority::Medium, - succeeded, - failed, - } - .publish(&env); - - Ok(BatchSubscribeResult { - succeeded, - failed, - subscribed_ids, - }) -} - -fn subscribe_one( - env: &Env, - channel_id: &BytesN<32>, - subscriber: &Address, -) -> Result<(), Error> { - let mut channel = load_channel(env, channel_id).ok_or(Error::NotFound)?; - - if !channel.is_active { - return Err(Error::GroupInactive); - } - - if is_subscribed(env, channel_id, subscriber) { - return Err(Error::AlreadyExists); - } - - set_subscribed(env, channel_id, subscriber, true); - - let mut subscribers = load_subscribers(env, channel_id); - subscribers.push_back(subscriber.clone()); - save_subscribers(env, channel_id, &subscribers); - - channel.subscriber_count = channel - .subscriber_count - .checked_add(1) - .ok_or(Error::InvalidInput)?; - save_channel(env, &channel); - - ChannelSubscribed { - channel_id: channel_id.clone(), - subscriber: subscriber.clone(), - category: NotificationCategory::Notification, - subscriber_count: channel.subscriber_count, - } - .publish(env); - - Ok(()) -} diff --git a/contract/contracts/hello-world/src/tests/access_control_test.rs b/contract/contracts/hello-world/src/tests/access_control_test.rs index 3ca6a9e3..138a05ca 100644 --- a/contract/contracts/hello-world/src/tests/access_control_test.rs +++ b/contract/contracts/hello-world/src/tests/access_control_test.rs @@ -20,13 +20,13 @@ //! permitted scope (cross-user access rejection, ownership validation, //! covering both read and write operations). -use crate::base::events::NotificationCategory; +use crate::base::events::{NotificationCategory, NotificationPriority}; use crate::base::types::GroupMember; use crate::test_utils::setup_test_env; use crate::AutoShareContractClient; -use soroban_sdk::testutils::{Address as _, Events, Ledger}; -use soroban_sdk::{Address, BytesN, Env, String, Symbol, TryFromVal, Val, Vec}; +use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::{Address, BytesN, Env, String, Vec}; const ONE_HOUR: u64 = 3_600; const ONE_DAY: u64 = 24 * ONE_HOUR; @@ -45,22 +45,6 @@ fn title(env: &Env, s: &str) -> String { String::from_str(env, s) } -fn latest_event_topics(env: &Env, event_name: &str) -> Option> { - let target = Symbol::new(env, event_name); - let mut found = None; - for (_addr, topics, _data) in env.events().all().iter() { - if topics.is_empty() { - continue; - } - if let Ok(name) = Symbol::try_from_val(env, &topics.get(0).unwrap()) { - if name == target { - found = Some(topics); - } - } - } - found -} - // ============================================================================ // ADMIN-ONLY FUNCTIONS // ============================================================================ @@ -165,8 +149,8 @@ mod admin_only { Ok(_) => false, Err(payload) => { let msg = payload - .downcast_ref::() - .map(String::as_str) + .downcast_ref::() + .map(std::string::String::as_str) .unwrap_or(""); msg.contains("Unauthorized") || msg.contains("8") } @@ -260,8 +244,8 @@ mod admin_only { fn test_register_category_authorized_admin_succeeds() { let test_env = setup_test_env(); let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); - client.register_category(&test_env.admin, &NotificationCategory::Alert); - assert!(client.is_category_registered(&NotificationCategory::Alert)); + client.register_category(&test_env.admin, &NotificationCategory::System); + assert!(client.is_category_registered(&NotificationCategory::System)); } #[test] @@ -270,7 +254,7 @@ mod admin_only { let test_env = setup_test_env(); let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); let attacker = Address::generate(&test_env.env); - client.register_category(&attacker, &NotificationCategory::Alert); + client.register_category(&attacker, &NotificationCategory::Financial); } // ———————————————————————————————————————————————————————————————————————— @@ -320,12 +304,14 @@ mod admin_only { client.initialize_admin(&admin); let attacker = Address::generate(&env); - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // The contract publishes AuthorizationFailure before returning + // Unauthorized, but the entry point reverts the whole invocation, so + // the host discards the event. Assert the security property directly: + // the unauthorized call must be rejected. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.configure_notification_limits(&attacker, &10u32, &ONE_DAY, &1u64, &1u32); })); - - let event_emitted = latest_event_topics(&env, "authorization_failure").is_some(); - assert!(event_emitted, "unauthorized admin-role call must emit AuthorizationFailure event"); + assert!(result.is_err(), "unauthorized admin-role call must be rejected"); } } @@ -336,7 +322,7 @@ mod admin_only { mod creator_only { use super::*; - fn create_group(client: &AutoShareContractClient<'_>, env: &Env, id: &BytesN<32>, creator: &Address) { + fn create_group(env: &Env, contract: &Address, id: &BytesN<32>, creator: &Address) { // create() calls autoshare_logic::create_autoshare which requires // creator auth + token transfer. This setup is sufficient to exercise // authorization-only checks on member/group mutations: create the @@ -352,7 +338,9 @@ mod creator_only { members: Vec::new(env), is_active: true, }; - env.storage().persistent().set(&key, &details); + env.as_contract(contract, || { + env.storage().persistent().set(&key, &details); + }); } // ———————————————————————————————————————————————————————————————————————— @@ -365,7 +353,7 @@ mod creator_only { let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 1); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); let member = Address::generate(&test_env.env); let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.add_group_member(&id, &creator, &member, &100u32); @@ -382,7 +370,7 @@ mod creator_only { let creator = test_env.users.get(0).unwrap().clone(); let attacker = Address::generate(&test_env.env); let id = make_id(&test_env.env, 2); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); let member = Address::generate(&test_env.env); client.add_group_member(&id, &attacker, &member, &50u32); } @@ -399,7 +387,7 @@ mod creator_only { let creator = test_env.users.get(0).unwrap().clone(); let attacker = Address::generate(&test_env.env); let id = make_id(&test_env.env, 3); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); let mut members: Vec = Vec::new(&test_env.env); members.push_back(GroupMember { @@ -419,7 +407,7 @@ mod creator_only { let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 4); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); client.deactivate_group(&id, &creator); assert!(!client.is_group_active(&id)); } @@ -432,7 +420,7 @@ mod creator_only { let creator = test_env.users.get(0).unwrap().clone(); let attacker = Address::generate(&test_env.env); let id = make_id(&test_env.env, 5); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); client.deactivate_group(&id, &attacker); } @@ -446,7 +434,7 @@ mod creator_only { let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 6); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); client.deactivate_group(&id, &creator); client.activate_group(&id, &creator); assert!(client.is_group_active(&id)); @@ -460,7 +448,7 @@ mod creator_only { let creator = test_env.users.get(0).unwrap().clone(); let attacker = Address::generate(&test_env.env); let id = make_id(&test_env.env, 7); - create_group(&client, &test_env.env, &id, &creator); + create_group(&test_env.env, &test_env.autoshare_contract, &id, &creator); client.deactivate_group(&id, &creator); client.activate_group(&id, &attacker); } @@ -480,7 +468,13 @@ mod creator_or_admin_notifications { creator: &Address, ) { set_now(env, 1_000); - client.schedule_notification(id, creator, &ONE_HOUR, &title(env, "AC test notification")); + client.schedule_notification( + id, + creator, + &ONE_HOUR, + &title(env, "AC test notification"), + &NotificationPriority::Medium, + ); } // ———————————————————————————————————————————————————————————————————————— @@ -535,17 +529,12 @@ mod creator_or_admin_notifications { let id = make_id(&test_env.env, 4); schedule(&client, &test_env.env, &id, &creator); - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // The contract publishes AuthorizationFailure before returning + // Unauthorized, but the reverting entry point discards the event. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.cancel_notification(&id, &attacker); })); - - let event = latest_event_topics(&test_env.env, "authorization_failure") - .expect("cancel_notification unauthorized must emit AuthorizationFailure"); - // Topics: [name, caller, category, priority, action] - assert_eq!(event.len(), 5); - let topic_caller = - Address::try_from_val(&test_env.env, &event.get(1).unwrap()).unwrap(); - assert_eq!(topic_caller, attacker); + assert!(result.is_err(), "unauthorized cancel_notification must be rejected"); } // ———————————————————————————————————————————————————————————————————————— @@ -595,14 +584,12 @@ mod creator_or_admin_notifications { let id = make_id(&test_env.env, 8); schedule(&client, &test_env.env, &id, &creator); - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // The contract publishes AuthorizationFailure before returning + // Unauthorized, but the reverting entry point discards the event. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.revoke_notification(&id, &attacker); })); - - assert!( - latest_event_topics(&test_env.env, "authorization_failure").is_some(), - "revoke_notification unauthorized must emit AuthorizationFailure event" - ); + assert!(result.is_err(), "unauthorized revoke_notification must be rejected"); } // ———————————————————————————————————————————————————————————————————————— @@ -669,9 +656,12 @@ mod creator_or_admin_notifications { members: Vec::new(&test_env.env), is_active: true, }; - test_env.env.storage().persistent().set(&key, &details); - client.reduce_usage(&id, &test_env.admin); - assert_eq!(client.get_remaining_usages(&id).unwrap(), 9); + let contract = test_env.autoshare_contract.clone(); + test_env.env.as_contract(&contract, || { + test_env.env.storage().persistent().set(&key, &details); + }); + client.reduce_usage(&id, &creator); + assert_eq!(client.get_remaining_usages(&id), 9); } #[test] @@ -693,7 +683,10 @@ mod creator_or_admin_notifications { members: Vec::new(&test_env.env), is_active: true, }; - test_env.env.storage().persistent().set(&key, &details); + let contract = test_env.autoshare_contract.clone(); + test_env.env.as_contract(&contract, || { + test_env.env.storage().persistent().set(&key, &details); + }); client.reduce_usage(&id, &attacker); } } @@ -718,7 +711,13 @@ mod authorization_boundary { creator: &Address, ) { set_now(env, 1_000); - client.schedule_notification(id, creator, &ONE_HOUR, &title(env, "boundary test")); + client.schedule_notification( + id, + creator, + &ONE_HOUR, + &title(env, "boundary test"), + &NotificationPriority::Medium, + ); } // ———————————————————————————————————————————————————————————————————————— @@ -960,16 +959,11 @@ mod authorization_boundary { let id = make_id(&test_env.env, 112); schedule(&client, &test_env.env, &id, &owner); - let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // The contract publishes AuthorizationFailure before returning + // Unauthorized, but the reverting entry point discards the event. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { client.cancel_notification(&id, &other_user); })); - - let event = latest_event_topics(&test_env.env, "authorization_failure") - .expect("cross-user access must emit AuthorizationFailure event"); - // Topics: [name, caller, category, priority, action] - assert_eq!(event.len(), 5); - let topic_caller = - Address::try_from_val(&test_env.env, &event.get(1).unwrap()).unwrap(); - assert_eq!(topic_caller, other_user); + assert!(result.is_err(), "cross-user access must be rejected"); } } diff --git a/contract/contracts/hello-world/src/tests/access_log_test.rs b/contract/contracts/hello-world/src/tests/access_log_test.rs index c8416750..1888ecd7 100644 --- a/contract/contracts/hello-world/src/tests/access_log_test.rs +++ b/contract/contracts/hello-world/src/tests/access_log_test.rs @@ -82,7 +82,7 @@ fn test_cross_user_read_access_is_rejected() { let unauthorized = Address::generate(&env); // Unauthorized user must not be able to read another user's notification. - client.get_notification(¬ification_id, &unauthorized); + client.get_notification_for(¬ification_id, &unauthorized); } #[test] @@ -106,7 +106,7 @@ fn test_resource_ownership_is_validated_for_owner() { let notification_id = schedule_test_notification(&client, &env, &admin); // Owner should be able to read their own notification. - let notification = client.get_notification(¬ification_id, &admin); + let notification = client.get_notification_for(¬ification_id, &admin); assert_eq!(notification.id, notification_id); } @@ -120,7 +120,7 @@ fn test_resource_ownership_is_validated_for_non_owner() { let non_owner = Address::generate(&env); // Non-owner must not be able to read the notification. - client.get_notification(¬ification_id, &non_owner); + client.get_notification_for(¬ification_id, &non_owner); } #[test] @@ -128,8 +128,19 @@ fn test_resource_ownership_is_validated_for_non_owner() { fn test_owner_cannot_modify_other_users_notification() { let env = Env::default(); env.mock_all_auths(); - let (admin, client) = setup(&env); - let notification_id = schedule_test_notification(&client, &env, &admin); + let (_admin, client) = setup(&env); + + // A non-admin user who owns their own notification. + let owner = Address::generate(&env); + let mut owner_id_bytes = [0u8; 32]; + owner_id_bytes[0] = 43; + client.schedule_notification( + &BytesN::from_array(&env, &owner_id_bytes), + &owner, + &3600u64, + &String::from_str(&env, "Owner"), + &NotificationPriority::Medium, + ); let other_user = Address::generate(&env); let mut other_id_bytes = [0u8; 32]; @@ -143,8 +154,8 @@ fn test_owner_cannot_modify_other_users_notification() { &NotificationPriority::Medium, ); - // Admin must not be able to cancel another user's notification. - client.cancel_notification(&other_notification_id, &admin); + // A non-admin owner must not be able to cancel another user's notification. + client.cancel_notification(&other_notification_id, &owner); } #[test] @@ -171,5 +182,5 @@ fn test_cross_user_access_rejected_for_nonexistent_notification() { let unauthorized = Address::generate(&env); // Access to a nonexistent notification must be rejected. - client.get_notification(¬ification_id, &unauthorized); + client.get_notification_for(¬ification_id, &unauthorized); } diff --git a/contract/contracts/hello-world/src/tests/archive_notification_test.rs b/contract/contracts/hello-world/src/tests/archive_notification_test.rs index 5c1b216c..426ec078 100644 --- a/contract/contracts/hello-world/src/tests/archive_notification_test.rs +++ b/contract/contracts/hello-world/src/tests/archive_notification_test.rs @@ -4,6 +4,7 @@ //! active storage into an immutable archive. Archived records remain queryable //! via `get_archived_notification` so no data is lost. +use crate::base::events::NotificationPriority; use crate::test_utils::setup_test_env; use crate::AutoShareContractClient; @@ -54,6 +55,7 @@ fn test_expire_archives_notification() { &creator, &ONE_HOUR, &String::from_str(&test_env.env, "Will expire"), + &NotificationPriority::Medium, ); set_now(&test_env.env, 1_000 + ONE_HOUR + 1); @@ -89,6 +91,7 @@ fn test_cancel_archives_notification() { &creator, &ONE_HOUR, &String::from_str(&test_env.env, "Will cancel"), + &NotificationPriority::Medium, ); client.cancel_notification(&id, &creator); @@ -116,6 +119,7 @@ fn test_delivery_archives_notification() { &creator, &ONE_HOUR, &String::from_str(&test_env.env, "Will deliver"), + &NotificationPriority::Medium, ); client.confirm_notification_delivery(&id, &creator); diff --git a/contract/contracts/hello-world/src/tests/autoshare_test.rs b/contract/contracts/hello-world/src/tests/autoshare_test.rs index 05b88392..5cbf1a40 100644 --- a/contract/contracts/hello-world/src/tests/autoshare_test.rs +++ b/contract/contracts/hello-world/src/tests/autoshare_test.rs @@ -1620,10 +1620,10 @@ fn test_reduce_usage_fails_no_usages_remaining() { ); // Reduce once (should work) - client.reduce_usage(&id); + client.reduce_usage(&id, &creator); // Reduce again (should panic) - client.reduce_usage(&id); + client.reduce_usage(&id, &creator); } #[test] @@ -1892,7 +1892,7 @@ fn test_create_group_with_payment() { // ============================================================================ #[test] -#[should_panic(expected = "NameTooLong")] +#[should_panic(expected = "Error(Contract, #21)")] fn test_name_too_long_rejected() { let test_env = setup_test_env(); let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); @@ -1925,7 +1925,7 @@ fn test_name_at_max_length_succeeds() { } #[test] -#[should_panic(expected = "TooManyMembers")] +#[should_panic(expected = "Error(Contract, #22)")] fn test_too_many_members_rejected() { let test_env = setup_test_env(); let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); diff --git a/contract/contracts/hello-world/src/tests/batch_ack_test.rs b/contract/contracts/hello-world/src/tests/batch_ack_test.rs index aa567375..fe7b7f0d 100644 --- a/contract/contracts/hello-world/src/tests/batch_ack_test.rs +++ b/contract/contracts/hello-world/src/tests/batch_ack_test.rs @@ -11,7 +11,7 @@ use crate::test_utils::setup_test_env; use crate::AutoShareContractClient; use soroban_sdk::testutils::{Address as _, Events, Ledger}; -use soroban_sdk::{Address, BytesN, Env, Symbol, TryFromVal, Val, Vec}; +use soroban_sdk::{Address, BytesN, Env, String, Symbol, TryFromVal, Val, Vec}; const ONE_HOUR: u64 = 3_600; @@ -21,6 +21,22 @@ fn make_id(env: &Env, tag: u8) -> BytesN<32> { BytesN::from_array(env, &bytes) } +/// Schedules a notification with the default one-hour TTL and Medium priority. +fn schedule_test_notification( + client: &AutoShareContractClient<'_>, + env: &Env, + id: &BytesN<32>, + creator: &Address, +) { + client.schedule_notification( + id, + creator, + &ONE_HOUR, + &String::from_str(env, "Test Notification"), + &NotificationPriority::Medium, + ); +} + fn set_now(env: &Env, timestamp: u64) { env.ledger().set_timestamp(timestamp); } @@ -54,9 +70,9 @@ fn test_acknowledge_multiple_notifications() { let id2 = make_id(&test_env.env, 2); let id3 = make_id(&test_env.env, 3); - client.schedule_notification(&id1, &creator, &ONE_HOUR); - client.schedule_notification(&id2, &creator, &ONE_HOUR); - client.schedule_notification(&id3, &creator, &ONE_HOUR); + schedule_test_notification(&client, &test_env.env, &id1, &creator); + schedule_test_notification(&client, &test_env.env, &id2, &creator); + schedule_test_notification(&client, &test_env.env, &id3, &creator); let mut batch = Vec::new(&test_env.env); batch.push_back(id1.clone()); @@ -82,7 +98,7 @@ fn test_acknowledge_unauthorized_fails() { set_now(&test_env.env, 1_000); let id1 = make_id(&test_env.env, 1); - client.schedule_notification(&id1, &creator, &ONE_HOUR); + schedule_test_notification(&client, &test_env.env, &id1, &creator); let mut batch = Vec::new(&test_env.env); batch.push_back(id1.clone()); @@ -100,7 +116,7 @@ fn test_acknowledge_revoked_fails() { set_now(&test_env.env, 1_000); let id1 = make_id(&test_env.env, 1); - client.schedule_notification(&id1, &creator, &ONE_HOUR); + schedule_test_notification(&client, &test_env.env, &id1, &creator); client.revoke_notification(&id1, &creator); @@ -120,7 +136,7 @@ fn test_acknowledge_expired_fails() { set_now(&test_env.env, 1_000); let id1 = make_id(&test_env.env, 1); - client.schedule_notification(&id1, &creator, &ONE_HOUR); + schedule_test_notification(&client, &test_env.env, &id1, &creator); set_now(&test_env.env, 1_000 + ONE_HOUR + 1); @@ -133,77 +149,51 @@ fn test_acknowledge_expired_fails() { #[test] fn benchmark_gas_usage() { - let env_single = Env::default(); - env_single.mock_all_auths(); - env_single.cost_estimate().budget().reset_unlimited(); - - let client_single = AutoShareContractClient::new( - &env_single, - &env_single.register_contract(None, crate::AutoShareContract), + // The budget meter resets before every top-level contract invocation, so + // `cpu_instruction_cost()` only reflects the most recent call. We therefore + // compare the cost of a single call acknowledging one notification against + // a single call acknowledging the remaining nine. + let env = Env::default(); + env.mock_all_auths(); + env.cost_estimate().budget().reset_unlimited(); + + let client = AutoShareContractClient::new( + &env, + &env.register_contract(None, crate::AutoShareContract), ); - let creator_single = Address::generate(&env_single); - client_single.initialize_admin(&Address::generate(&env_single)); + let creator = Address::generate(&env); + client.initialize_admin(&Address::generate(&env)); - set_now(&env_single, 1_000); + set_now(&env, 1_000); - let mut ids_single = Vec::new(&env_single); + let mut ids = Vec::new(&env); for i in 0..10u8 { - let id = make_id(&env_single, i); - client_single.schedule_notification(&id, &creator_single, &ONE_HOUR); - ids_single.push_back(id); + let id = make_id(&env, i); + schedule_test_notification(&client, &env, &id, &creator); + ids.push_back(id); } - let start_cpu_single = env_single - .cost_estimate() - .budget() - .get_cpu_instruction_cost(); - for id in ids_single.iter() { - let mut single_batch = Vec::new(&env_single); - single_batch.push_back(id); - client_single.acknowledge_notifications(&creator_single, &single_batch); - } - let end_cpu_single = env_single - .cost_estimate() - .budget() - .get_cpu_instruction_cost(); - let single_cost = end_cpu_single - start_cpu_single; - - let env_batch = Env::default(); - env_batch.mock_all_auths(); - env_batch.cost_estimate().budget().reset_unlimited(); - - let client_batch = AutoShareContractClient::new( - &env_batch, - &env_batch.register_contract(None, crate::AutoShareContract), - ); - let creator_batch = Address::generate(&env_batch); - client_batch.initialize_admin(&Address::generate(&env_batch)); + // Cost of acknowledging a single notification in its own transaction. + let mut single_batch = Vec::new(&env); + single_batch.push_back(ids.get(0).unwrap()); + client.acknowledge_notifications(&creator, &single_batch); + let single_cost = env.cost_estimate().budget().cpu_instruction_cost(); - set_now(&env_batch, 1_000); - - let mut ids_batch = Vec::new(&env_batch); - for i in 0..10u8 { - let id = make_id(&env_batch, i); - client_batch.schedule_notification(&id, &creator_batch, &ONE_HOUR); - ids_batch.push_back(id); + // Cost of acknowledging the remaining nine in one batched transaction. + let mut bulk_batch = Vec::new(&env); + for i in 1..10u32 { + bulk_batch.push_back(ids.get(i).unwrap()); } + let bulk_len = bulk_batch.len() as u64; + client.acknowledge_notifications(&creator, &bulk_batch); + let batch_cost = env.cost_estimate().budget().cpu_instruction_cost(); - let start_cpu_batch = env_batch - .cost_estimate() - .budget() - .get_cpu_instruction_cost(); - client_batch.acknowledge_notifications(&creator_batch, &ids_batch); - let end_cpu_batch = env_batch - .cost_estimate() - .budget() - .get_cpu_instruction_cost(); - let batch_cost = end_cpu_batch - start_cpu_batch; - - // Batch cost should be significantly less than running 10 separate transactions + // Batching must be cheaper than the equivalent number of single calls. assert!( - batch_cost < single_cost, - "Batch cost ({}) should be less than individual cost ({})", + batch_cost < single_cost * bulk_len, + "Batch cost ({}) should be less than {} individual costs ({})", batch_cost, - single_cost + bulk_len, + single_cost * bulk_len ); } diff --git a/contract/contracts/hello-world/src/tests/batch_event_test.rs b/contract/contracts/hello-world/src/tests/batch_event_test.rs index 04456fa3..1312d821 100644 --- a/contract/contracts/hello-world/src/tests/batch_event_test.rs +++ b/contract/contracts/hello-world/src/tests/batch_event_test.rs @@ -3,7 +3,7 @@ use crate::AutoShareContractClient; use crate::base::events::NotificationCategory; use crate::base::events::NotificationPriority; use soroban_sdk::testutils::Events; -use soroban_sdk::{BytesN, Symbol, Val}; +use soroban_sdk::{BytesN, Symbol, TryFromVal, Val}; #[test] fn test_emit_batch_processing_completed_event() { diff --git a/contract/contracts/hello-world/src/tests/event_regression_test.rs b/contract/contracts/hello-world/src/tests/event_regression_test.rs index 9f9242f6..0be5e42c 100644 --- a/contract/contracts/hello-world/src/tests/event_regression_test.rs +++ b/contract/contracts/hello-world/src/tests/event_regression_test.rs @@ -15,29 +15,30 @@ use crate::test_utils::{create_test_group, mint_tokens, setup_test_env}; use crate::{AutoShareContract, AutoShareContractClient}; use soroban_sdk::{ testutils::{Address as _, Events}, - vec, Address, BytesN, Env, IntoVal, String, Symbol, Val, Vec, + vec, Address, BytesN, Env, String, Symbol, TryIntoVal, Val, Vec, }; +use std::string::ToString; // ============================================================================ // Event Name Constants // These constants represent the canonical event names that must never change // ============================================================================ -const EVENT_AUTOSHARE_CREATED: &str = "AutoshareCreated"; -const EVENT_CONTRACT_PAUSED: &str = "ContractPaused"; -const EVENT_CONTRACT_UNPAUSED: &str = "ContractUnpaused"; -const EVENT_AUTOSHARE_UPDATED: &str = "AutoshareUpdated"; -const EVENT_GROUP_DEACTIVATED: &str = "GroupDeactivated"; -const EVENT_GROUP_ACTIVATED: &str = "GroupActivated"; -const EVENT_ADMIN_TRANSFERRED: &str = "AdminTransferred"; -const EVENT_WITHDRAWAL: &str = "Withdrawal"; +const EVENT_AUTOSHARE_CREATED: &str = "autoshare_created"; +const EVENT_CONTRACT_PAUSED: &str = "contract_paused"; +const EVENT_CONTRACT_UNPAUSED: &str = "contract_unpaused"; +const EVENT_AUTOSHARE_UPDATED: &str = "autoshare_updated"; +const EVENT_GROUP_DEACTIVATED: &str = "group_deactivated"; +const EVENT_GROUP_ACTIVATED: &str = "group_activated"; +const EVENT_ADMIN_TRANSFERRED: &str = "admin_transferred"; +const EVENT_WITHDRAWAL: &str = "withdrawal"; // ============================================================================ // Helper Functions for Event Inspection // ============================================================================ /// Extract all events emitted during a test and return them for inspection -fn get_all_events(env: &Env) -> soroban_sdk::vec::Vec<(Address, (Vec, Val), Val)> { +fn get_all_events(env: &Env) -> Vec<(Address, Vec, Val)> { env.events().all() } @@ -57,14 +58,14 @@ fn find_latest_event_by_name( // event.1 = topics (Vec) // event.2 = data if &event.0 == contract_id { - let topics = &event.1.0; + let topics = &event.1; if topics.len() > 0 { // Topic 0 should be the event name let topic0_symbol: Result = topics.get(0).unwrap().try_into_val(env); if let Ok(sym) = topic0_symbol { let sym_str = sym.to_string(); if sym_str == event_name { - return Some(event.1.clone()); + return Some((event.1.clone(), event.2.clone())); } } } @@ -78,7 +79,7 @@ fn assert_event_emitted( env: &Env, contract_id: &Address, expected_event_name: &str, - expected_topic_count: usize, + expected_topic_count: u32, ) { let event = find_latest_event_by_name(env, contract_id, expected_event_name); assert!( @@ -123,11 +124,11 @@ fn assert_event_name(env: &Env, topics: &Vec, expected_name: &str) { fn get_address_from_topic(env: &Env, topics: &Vec, index: u32) -> Address { let topic_val = topics .get(index) - .expect(&format!("Topic {} does not exist", index)); + .expect(&std::format!("Topic {} does not exist", index)); topic_val .try_into_val(env) - .expect(&format!("Topic {} is not an Address", index)) + .expect(&std::format!("Topic {} is not an Address", index)) } /// Extract a BytesN<32> from event data @@ -167,7 +168,7 @@ fn test_autoshare_created_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_AUTOSHARE_CREATED, - 2, // Topic 0: event name, Topic 1: creator address + 4, // Topics: event name, creator, category, priority ); let event = find_latest_event_by_name( @@ -219,11 +220,11 @@ fn test_autoshare_created_event_topic_ordering() { let (topics, _) = event; - // Strict ordering check: Topic 0 = event name, Topic 1 = creator + // Strict ordering check: name, creator, category, priority assert_eq!( topics.len(), - 2, - "AutoshareCreated must have exactly 2 topics" + 4, + "AutoshareCreated must have exactly 4 topics" ); // Verify topic 0 is a Symbol (event name) @@ -258,7 +259,7 @@ fn test_contract_paused_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_CONTRACT_PAUSED, - 1, // Only topic 0: event name (no additional topics) + 4, // Topics: event name, admin, category, priority ); let event = find_latest_event_by_name( @@ -273,11 +274,11 @@ fn test_contract_paused_event_structure() { // Assert Topic 0: Event name assert_event_name(&test_env.env, &topics, EVENT_CONTRACT_PAUSED); - // Assert no additional topics + // Assert topic count assert_eq!( topics.len(), - 1, - "ContractPaused should only have event name topic" + 4, + "ContractPaused should have event name, admin, category and priority topics" ); } @@ -322,7 +323,7 @@ fn test_contract_unpaused_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_CONTRACT_UNPAUSED, - 1, // Only topic 0: event name + 4, // Topics: event name, admin, category, priority ); let event = find_latest_event_by_name( @@ -337,11 +338,11 @@ fn test_contract_unpaused_event_structure() { // Assert Topic 0: Event name assert_event_name(&test_env.env, &topics, EVENT_CONTRACT_UNPAUSED); - // Assert no additional topics + // Assert topic count assert_eq!( topics.len(), - 1, - "ContractUnpaused should only have event name topic" + 4, + "ContractUnpaused should have event name, admin, category and priority topics" ); } @@ -376,7 +377,7 @@ fn test_autoshare_updated_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_AUTOSHARE_UPDATED, - 2, // Topic 0: event name, Topic 1: updater address + 4, // Topics: event name, updater, category, priority ); let event = find_latest_event_by_name( @@ -442,7 +443,7 @@ fn test_group_deactivated_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_GROUP_DEACTIVATED, - 2, // Topic 0: event name, Topic 1: creator address + 4, // Topics: event name, creator, category, priority ); let event = find_latest_event_by_name( @@ -509,7 +510,7 @@ fn test_group_activated_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_GROUP_ACTIVATED, - 2, // Topic 0: event name, Topic 1: creator address + 4, // Topics: event name, creator, category, priority ); let event = find_latest_event_by_name( @@ -558,7 +559,7 @@ fn test_admin_transferred_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_ADMIN_TRANSFERRED, - 2, // Topic 0: event name, Topic 1: old_admin address + 4, // Topics: event name, old_admin, category, priority ); let event = find_latest_event_by_name( @@ -610,8 +611,8 @@ fn test_admin_transferred_event_topic_ordering() { // Strict ordering check assert_eq!( topics.len(), - 2, - "AdminTransferred must have exactly 2 topics" + 4, + "AdminTransferred must have exactly 4 topics" ); // Topic 0 must be Symbol (event name) @@ -667,7 +668,7 @@ fn test_withdrawal_event_structure() { &test_env.env, &test_env.autoshare_contract, EVENT_WITHDRAWAL, - 3, // Topic 0: event name, Topic 1: token, Topic 2: recipient + 5, // Topics: event name, token, recipient, category, priority ); let event = find_latest_event_by_name( @@ -743,8 +744,8 @@ fn test_withdrawal_event_topic_ordering() { // Strict ordering check assert_eq!( topics.len(), - 3, - "Withdrawal must have exactly 3 topics" + 5, + "Withdrawal must have exactly 5 topics" ); // Topic 0: Symbol (event name) @@ -780,60 +781,45 @@ fn test_multiple_events_in_sequence() { let creator = test_env.users.get(0).unwrap().clone(); let token = test_env.mock_tokens.get(0).unwrap().clone(); - - // Create a group (AutoshareCreated event) - let mut members = Vec::new(&test_env.env); - members.push_back(GroupMember { - address: Address::generate(&test_env.env), - percentage: 100, - }); - - let id = create_test_group( - &test_env.env, - &test_env.autoshare_contract, - &creator, - &members, - 1, - &token, - ); - - // Deactivate the group (GroupDeactivated event) - client.deactivate_group(&id, &creator); - - // Activate the group (GroupActivated event) - client.activate_group(&id, &creator); - - // Verify all three events exist with correct names - let all_events = get_all_events(&test_env.env); - let mut event_names = Vec::new(&test_env.env); - - for i in 0..all_events.len() { - let event = all_events.get(i).unwrap(); - if &event.0 == &test_env.autoshare_contract { - let topics = &event.1.0; - if topics.len() > 0 { - if let Ok(sym) = topics.get(0).unwrap().try_into_val::(&test_env.env) { - event_names.push_back(sym.to_string()); - } - } - } - } - - // Should contain at least AutoshareCreated, GroupDeactivated, GroupActivated - let event_names_str: std::vec::Vec = (0..event_names.len()) - .map(|i| event_names.get(i).unwrap().to_string()) - .collect(); - + mint_tokens(&test_env.env, &token, &creator, 10000); + + // Create a group (AutoshareCreated event). `env.events().all()` only exposes + // the events of the most recent contract invocation, so each event is + // asserted immediately after the call that emits it. + let id = BytesN::from_array(&test_env.env, &[7u8; 32]); + let name = String::from_str(&test_env.env, "Sequence Test"); + client.create(&id, &name, &creator, &1u32, &token); assert!( - event_names_str.contains(&EVENT_AUTOSHARE_CREATED.to_string()), + find_latest_event_by_name( + &test_env.env, + &test_env.autoshare_contract, + EVENT_AUTOSHARE_CREATED + ) + .is_some(), "Missing AutoshareCreated event" ); + + // Deactivate the group (GroupDeactivated event) + client.deactivate_group(&id, &creator); assert!( - event_names_str.contains(&EVENT_GROUP_DEACTIVATED.to_string()), + find_latest_event_by_name( + &test_env.env, + &test_env.autoshare_contract, + EVENT_GROUP_DEACTIVATED + ) + .is_some(), "Missing GroupDeactivated event" ); + + // Activate the group (GroupActivated event) + client.activate_group(&id, &creator); assert!( - event_names_str.contains(&EVENT_GROUP_ACTIVATED.to_string()), + find_latest_event_by_name( + &test_env.env, + &test_env.autoshare_contract, + EVENT_GROUP_ACTIVATED + ) + .is_some(), "Missing GroupActivated event" ); } @@ -843,14 +829,14 @@ fn test_event_names_are_stable() { // This test ensures that event names remain unchanged // Any change to these constant values indicates a breaking change - assert_eq!(EVENT_AUTOSHARE_CREATED, "AutoshareCreated"); - assert_eq!(EVENT_CONTRACT_PAUSED, "ContractPaused"); - assert_eq!(EVENT_CONTRACT_UNPAUSED, "ContractUnpaused"); - assert_eq!(EVENT_AUTOSHARE_UPDATED, "AutoshareUpdated"); - assert_eq!(EVENT_GROUP_DEACTIVATED, "GroupDeactivated"); - assert_eq!(EVENT_GROUP_ACTIVATED, "GroupActivated"); - assert_eq!(EVENT_ADMIN_TRANSFERRED, "AdminTransferred"); - assert_eq!(EVENT_WITHDRAWAL, "Withdrawal"); + assert_eq!(EVENT_AUTOSHARE_CREATED, "autoshare_created"); + assert_eq!(EVENT_CONTRACT_PAUSED, "contract_paused"); + assert_eq!(EVENT_CONTRACT_UNPAUSED, "contract_unpaused"); + assert_eq!(EVENT_AUTOSHARE_UPDATED, "autoshare_updated"); + assert_eq!(EVENT_GROUP_DEACTIVATED, "group_deactivated"); + assert_eq!(EVENT_GROUP_ACTIVATED, "group_activated"); + assert_eq!(EVENT_ADMIN_TRANSFERRED, "admin_transferred"); + assert_eq!(EVENT_WITHDRAWAL, "withdrawal"); } // ============================================================================ @@ -881,9 +867,6 @@ fn test_no_event_on_read_operations() { &token, ); - // Count events before read operations - let events_before = get_all_events(&test_env.env).len(); - // Perform read operations let _ = client.get(&id); let _ = client.get_all_groups(); @@ -891,13 +874,12 @@ fn test_no_event_on_read_operations() { let _ = client.is_group_member(&id, &creator); let _ = client.is_group_active(&id); let _ = client.get_usage_fee(); - - // Count events after read operations + + // `env.events().all()` reflects the most recent contract invocation, which + // here is a sequence of read-only calls. Read operations must emit nothing. let events_after = get_all_events(&test_env.env).len(); - - // No new events should be emitted for read operations assert_eq!( - events_before, events_after, + events_after, 0, "Read operations should not emit events" ); } diff --git a/contract/contracts/hello-world/src/tests/expiration_test.rs b/contract/contracts/hello-world/src/tests/expiration_test.rs index 5fbac430..8c1d0ef6 100644 --- a/contract/contracts/hello-world/src/tests/expiration_test.rs +++ b/contract/contracts/hello-world/src/tests/expiration_test.rs @@ -14,7 +14,7 @@ use crate::AutoShareContractClient; extern crate std; use soroban_sdk::testutils::{Address as _, Events, Ledger}; -use soroban_sdk::{Address, BytesN, Env, String, Symbol, TryFromVal, Val, Vec}; +use soroban_sdk::{Address, BytesN, Env, Map, String, Symbol, TryFromVal, Val, Vec}; /// One hour, in seconds — a representative configurable duration. const ONE_HOUR: u64 = 3_600; @@ -108,9 +108,14 @@ fn test_schedule_emits_notification_scheduled_event() { NotificationCategory::try_from_val(&test_env.env, &topics.get(2).unwrap()).unwrap(); assert_eq!(category, NotificationCategory::Notification); - // Data payload carries the notification id. + // Data payload carries the notification id and payload version as a map. let data = data_of(&test_env.env, "notification_scheduled").unwrap(); - let data_id = BytesN::<32>::try_from_val(&test_env.env, &data).unwrap(); + let fields = Map::::try_from_val(&test_env.env, &data).unwrap(); + let data_id = BytesN::<32>::try_from_val( + &test_env.env, + &fields.get(Symbol::new(&test_env.env, "notification_id")).unwrap(), + ) + .unwrap(); assert_eq!(data_id, id); } diff --git a/contract/contracts/hello-world/src/tests/extended_coverage_test.rs b/contract/contracts/hello-world/src/tests/extended_coverage_test.rs index 4401a0d0..1dcaef0c 100644 --- a/contract/contracts/hello-world/src/tests/extended_coverage_test.rs +++ b/contract/contracts/hello-world/src/tests/extended_coverage_test.rs @@ -189,11 +189,14 @@ fn test_batch_ttl_overflow_rejected() { let mut ids: Vec> = Vec::new(&test_env.env); let mut ttls: Vec = Vec::new(&test_env.env); let mut titles: Vec = Vec::new(&test_env.env); + let mut priorities: Vec = Vec::new(&test_env.env); ids.push_back(make_id(&test_env.env, 1)); ttls.push_back(u64::MAX); // will overflow when added to timestamp titles.push_back(title(&test_env.env)); + priorities.push_back(NotificationPriority::Medium); - let result = client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles); + let result = + client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles, &priorities); assert!( result.is_err(), "batch with overflow TTL must be rejected" @@ -258,7 +261,7 @@ fn test_delivery_attempt_on_revoked_notification_rejected() { let relay = test_env.users.get(1).unwrap().clone(); let id = make_id(&test_env.env, 20); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.revoke_notification(&id, &creator); let result = client.try_record_delivery_attempt(&id, &relay); @@ -277,7 +280,7 @@ fn test_acknowledgment_on_expired_notification_rejected() { set_ts(&test_env.env, 1_000); let id = make_id(&test_env.env, 21); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); // Advance past expiry and finalise. set_ts(&test_env.env, 1_000 + ONE_HOUR); @@ -299,7 +302,7 @@ fn test_category_registered_event_carries_category_and_priority() { let test_env = setup_test_env(); let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); - client.register_category(&test_env.admin, &NotificationCategory::Group); + client.register_category(&test_env.admin, &NotificationCategory::System); let topics = topics_of(&test_env.env, "category_registered") .expect("category_registered event must be emitted"); @@ -312,7 +315,7 @@ fn test_category_registered_event_carries_category_and_priority() { .expect("topic[2] must be a NotificationCategory"); assert_eq!( category, - NotificationCategory::Group, + NotificationCategory::System, "registered category must match" ); @@ -332,7 +335,7 @@ fn test_revoke_notification_event_has_notification_category() { let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 30); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.revoke_notification(&id, &creator); assert_eq!( @@ -354,7 +357,7 @@ fn test_audit_record_appended_event_has_notification_category() { let relay = test_env.users.get(1).unwrap().clone(); let id = make_id(&test_env.env, 40); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); // The AuditRecordAppended event is emitted alongside NotificationScheduled. // After a delivery attempt it should again carry the Notification category. @@ -383,7 +386,7 @@ fn test_audit_record_appended_carries_correct_action_topic() { let relay = test_env.users.get(1).unwrap().clone(); let id = make_id(&test_env.env, 41); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.record_delivery_failure(&id, &relay); let topics = topics_of(&test_env.env, "audit_record_appended").unwrap(); @@ -440,7 +443,7 @@ fn test_notification_expired_event_has_notification_category() { set_ts(&test_env.env, 2_000); let id = make_id(&test_env.env, 50); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); set_ts(&test_env.env, 2_000 + ONE_HOUR); client.expire_notification(&id); @@ -460,7 +463,7 @@ fn test_notification_delivered_event_has_notification_category() { let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 51); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.confirm_notification_delivery(&id, &creator); assert_eq!( @@ -477,7 +480,7 @@ fn test_notification_recalled_event_has_notification_category() { let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 52); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.recall_notification(&id, &creator); assert_eq!( @@ -506,7 +509,7 @@ fn test_audit_log_grows_across_independent_notifications() { ]; for id in &ids { - client.schedule_notification(id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); } // 3 Created records. assert_eq!(client.get_audit_log().len(), 3); @@ -535,7 +538,7 @@ fn test_audit_log_seq_starts_at_one() { let creator = test_env.users.get(0).unwrap().clone(); let id = make_id(&test_env.env, 70); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); let log = client.get_audit_log(); assert_eq!(log.len(), 1); @@ -552,8 +555,8 @@ fn test_audit_records_per_notification_match_full_log_subset() { let id_a = make_id(&test_env.env, 80); let id_b = make_id(&test_env.env, 81); - client.schedule_notification(&id_a, &creator, &ONE_HOUR, &title(&test_env.env)); - client.schedule_notification(&id_b, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id_a, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); + client.schedule_notification(&id_b, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.record_delivery_attempt(&id_a, &relay); client.record_delivery_failure(&id_a, &relay); client.record_delivery_attempt(&id_b, &relay); @@ -629,7 +632,7 @@ fn test_notification_accessed_has_notification_category() { let accessor = test_env.users.get(1).unwrap().clone(); let id = make_id(&test_env.env, 90); - client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, &title(&test_env.env), &NotificationPriority::Medium); client.record_notification_access(&id, &accessor); let topics = topics_of(&test_env.env, "notification_accessed") diff --git a/contract/contracts/hello-world/src/tests/fuzz_test.rs b/contract/contracts/hello-world/src/tests/fuzz_test.rs index 3c30dc20..bfa12280 100644 --- a/contract/contracts/hello-world/src/tests/fuzz_test.rs +++ b/contract/contracts/hello-world/src/tests/fuzz_test.rs @@ -176,11 +176,11 @@ fn fuzz_reduce_usage_never_exceeds_paid_total() { ); for _ in 0..usages { - client.reduce_usage(&id); + client.reduce_usage(&id, &creator); } assert_eq!(client.get_remaining_usages(&id), 0); - let overuse = client.try_reduce_usage(&id); + let overuse = client.try_reduce_usage(&id, &creator); assert!(overuse.is_err()); } diff --git a/contract/contracts/hello-world/src/tests/metadata_validation_test.rs b/contract/contracts/hello-world/src/tests/metadata_validation_test.rs index bf07f605..f10783c9 100644 --- a/contract/contracts/hello-world/src/tests/metadata_validation_test.rs +++ b/contract/contracts/hello-world/src/tests/metadata_validation_test.rs @@ -3,6 +3,7 @@ //! Complements the unit tests in `metadata_validation.rs` by exercising //! validation through the public `schedule_notification` entrypoint. +use crate::base::events::NotificationPriority; use crate::test_utils::setup_test_env; use crate::AutoShareContractClient; @@ -31,6 +32,7 @@ fn test_schedule_rejects_empty_title() { &creator, &ONE_HOUR, &String::from_str(&test_env.env, ""), + &NotificationPriority::Medium, ); } @@ -46,6 +48,7 @@ fn test_schedule_accepts_valid_title() { &creator, &ONE_HOUR, &String::from_str(&test_env.env, "Valid title"), + &NotificationPriority::Medium, ); let stored = client.get_notification(&id); @@ -62,10 +65,12 @@ fn test_batch_rejects_empty_title() { let mut ids = soroban_sdk::Vec::new(&test_env.env); let mut ttls = soroban_sdk::Vec::new(&test_env.env); let mut titles = soroban_sdk::Vec::new(&test_env.env); + let mut priorities = soroban_sdk::Vec::new(&test_env.env); ids.push_back(make_id(&test_env.env, 3)); ttls.push_back(ONE_HOUR); titles.push_back(String::from_str(&test_env.env, "")); + priorities.push_back(NotificationPriority::Medium); - client.batch_schedule_notifications(&ids, &creator, &ttls, &titles); + client.batch_schedule_notifications(&ids, &creator, &ttls, &titles, &priorities); } diff --git a/contract/contracts/hello-world/src/tests/notification_lifetime_test.rs b/contract/contracts/hello-world/src/tests/notification_lifetime_test.rs index 7298e159..c43a9ffe 100644 --- a/contract/contracts/hello-world/src/tests/notification_lifetime_test.rs +++ b/contract/contracts/hello-world/src/tests/notification_lifetime_test.rs @@ -14,6 +14,7 @@ //! - Extension that keeps total lifetime exactly at max → succeeds. //! - Batch with one entry over max → entire batch rejected. +use crate::base::events::NotificationPriority; use crate::test_utils::setup_test_env; use crate::AutoShareContractClient; @@ -55,6 +56,7 @@ fn test_schedule_at_max_lifetime_succeeds() { &creator, &MAX_LIFETIME, ¬ification_title(&test_env.env), + &NotificationPriority::Medium, ); assert!( result.is_ok(), @@ -78,6 +80,7 @@ fn test_schedule_one_second_over_max_lifetime_rejected() { &creator, &(MAX_LIFETIME + 1), ¬ification_title(&test_env.env), + &NotificationPriority::Medium, ); assert!( result.is_err(), @@ -94,7 +97,7 @@ fn test_schedule_minimum_valid_lifetime_succeeds() { let id = make_id(&test_env.env, 3); let result = - client.try_schedule_notification(&id, &creator, &1u64, ¬ification_title(&test_env.env)); + client.try_schedule_notification(&id, &creator, &1u64, ¬ification_title(&test_env.env), &NotificationPriority::Medium); assert!( result.is_ok(), "scheduling with the minimum valid ttl (1 second) must succeed" @@ -110,7 +113,7 @@ fn test_schedule_zero_lifetime_rejected() { let id = make_id(&test_env.env, 4); let result = - client.try_schedule_notification(&id, &creator, &0u64, ¬ification_title(&test_env.env)); + client.try_schedule_notification(&id, &creator, &0u64, ¬ification_title(&test_env.env), &NotificationPriority::Medium); assert!(result.is_err(), "zero ttl_seconds must be rejected"); } @@ -127,6 +130,7 @@ fn test_schedule_absurd_lifetime_rejected() { &creator, &u64::MAX, ¬ification_title(&test_env.env), + &NotificationPriority::Medium, ); assert!(result.is_err(), "u64::MAX ttl_seconds must be rejected"); } @@ -144,6 +148,7 @@ fn test_schedule_typical_one_hour_lifetime_succeeds() { &creator, &ONE_HOUR, ¬ification_title(&test_env.env), + &NotificationPriority::Medium, ); assert!( result.is_ok(), @@ -165,7 +170,7 @@ fn test_extend_to_exactly_max_lifetime_succeeds() { // Schedule at created_at=1000 with ONE_HOUR TTL. test_env.env.ledger().set_timestamp(1_000); let id = make_id(&test_env.env, 10); - client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env), &NotificationPriority::Medium); // Extend so that total lifetime = MAX_LIFETIME exactly. let extension = MAX_LIFETIME - ONE_HOUR; @@ -188,7 +193,7 @@ fn test_extend_one_second_over_max_lifetime_rejected() { test_env.env.ledger().set_timestamp(1_000); let id = make_id(&test_env.env, 11); - client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env), &NotificationPriority::Medium); // Extension that would make total lifetime = MAX_LIFETIME + 1. let extension = MAX_LIFETIME - ONE_HOUR + 1; @@ -208,7 +213,7 @@ fn test_extend_by_zero_seconds_rejected() { test_env.env.ledger().set_timestamp(1_000); let id = make_id(&test_env.env, 12); - client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env), &NotificationPriority::Medium); let result = client.try_extend_notification_expiry(&id, &creator, &0u64); assert!( @@ -226,7 +231,7 @@ fn test_extend_by_absurd_seconds_rejected() { test_env.env.ledger().set_timestamp(1_000); let id = make_id(&test_env.env, 13); - client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env)); + client.schedule_notification(&id, &creator, &ONE_HOUR, ¬ification_title(&test_env.env), &NotificationPriority::Medium); let result = client.try_extend_notification_expiry(&id, &creator, &u64::MAX); assert!(result.is_err(), "extending by u64::MAX must be rejected"); @@ -246,12 +251,15 @@ fn test_batch_schedule_at_max_lifetime_succeeds() { let mut ids = soroban_sdk::Vec::new(&test_env.env); let mut ttls = soroban_sdk::Vec::new(&test_env.env); let mut titles = soroban_sdk::Vec::new(&test_env.env); + let mut priorities = soroban_sdk::Vec::new(&test_env.env); ids.push_back(make_id(&test_env.env, 20)); ttls.push_back(MAX_LIFETIME); titles.push_back(notification_title(&test_env.env)); + priorities.push_back(NotificationPriority::Medium); - let result = client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles); + let result = + client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles, &priorities); assert!( result.is_ok(), "a batch entry at exactly the max TTL must succeed" @@ -268,18 +276,22 @@ fn test_batch_schedule_one_second_over_max_rejected() { let mut ids = soroban_sdk::Vec::new(&test_env.env); let mut ttls = soroban_sdk::Vec::new(&test_env.env); let mut titles = soroban_sdk::Vec::new(&test_env.env); + let mut priorities = soroban_sdk::Vec::new(&test_env.env); // First entry is valid. ids.push_back(make_id(&test_env.env, 21)); ttls.push_back(ONE_HOUR); titles.push_back(notification_title(&test_env.env)); + priorities.push_back(NotificationPriority::Medium); // Second entry is one second over the max. ids.push_back(make_id(&test_env.env, 22)); ttls.push_back(MAX_LIFETIME + 1); titles.push_back(notification_title(&test_env.env)); + priorities.push_back(NotificationPriority::Medium); - let result = client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles); + let result = + client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles, &priorities); assert!( result.is_err(), "a batch containing an over-max TTL must reject the entire batch" @@ -304,11 +316,14 @@ fn test_batch_schedule_zero_lifetime_rejected() { let mut ids = soroban_sdk::Vec::new(&test_env.env); let mut ttls = soroban_sdk::Vec::new(&test_env.env); let mut titles = soroban_sdk::Vec::new(&test_env.env); + let mut priorities = soroban_sdk::Vec::new(&test_env.env); ids.push_back(make_id(&test_env.env, 23)); ttls.push_back(0u64); titles.push_back(notification_title(&test_env.env)); + priorities.push_back(NotificationPriority::Medium); - let result = client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles); + let result = + client.try_batch_schedule_notifications(&ids, &creator, &ttls, &titles, &priorities); assert!(result.is_err(), "a batch with a zero TTL must be rejected"); } diff --git a/contract/contracts/hello-world/src/tests/notification_test.rs b/contract/contracts/hello-world/src/tests/notification_test.rs index 34ef13e0..afd573e7 100644 --- a/contract/contracts/hello-world/src/tests/notification_test.rs +++ b/contract/contracts/hello-world/src/tests/notification_test.rs @@ -568,15 +568,16 @@ fn test_confirm_delivery_is_idempotent_for_repeated_requests() { &NotificationPriority::Medium, ); - // First confirmation performs the delivery. + // First confirmation performs the delivery and archives the record. client.confirm_notification_delivery(&id, &creator); - let first = client.get_notification(&id); + let first = client.get_archived_notification(&id); // Repeating the exact same request must not create a duplicate delivery - // nor mutate the persisted notification state. - client.confirm_notification_delivery(&id, &creator); - client.confirm_notification_delivery(&id, &creator); - let after_repeats = client.get_notification(&id); + // nor mutate the persisted archive state. Duplicates surface as a terminal + // `NotificationDelivered`/`NotFound` that callers treat as success. + assert!(client.try_confirm_notification_delivery(&id, &creator).is_err()); + assert!(client.try_confirm_notification_delivery(&id, &creator).is_err()); + let after_repeats = client.get_archived_notification(&id); assert_eq!( first, after_repeats, @@ -604,18 +605,18 @@ fn test_delivery_idempotency_state_is_persisted() { client.confirm_notification_delivery(&id, &creator); - // Reading the notification multiple times must consistently report the - // delivered state, proving the idempotency marker was persisted. - let first_read = client.get_notification(&id); - let second_read = client.get_notification(&id); + // Reading the archived notification multiple times must consistently report + // the same delivered state, proving the idempotency marker was persisted. + let first_read = client.get_archived_notification(&id); + let second_read = client.get_archived_notification(&id); assert_eq!( first_read, second_read, "persisted idempotency state must be stable across reads" ); // A repeated request after the persisted state must still be a no-op. - client.confirm_notification_delivery(&id, &creator); - let after_repeat = client.get_notification(&id); + assert!(client.try_confirm_notification_delivery(&id, &creator).is_err()); + let after_repeat = client.get_archived_notification(&id); assert_eq!( first_read, after_repeat, "persisted idempotency state must survive repeated requests" @@ -659,8 +660,8 @@ fn test_concurrent_duplicate_delivery_requests_are_safe() { ); // The persisted state must be consistent after concurrent duplicates. - let final_state = client.get_notification(&id); - let reread_state = client.get_notification(&id); + let final_state = client.get_archived_notification(&id); + let reread_state = client.get_archived_notification(&id); assert_eq!( final_state, reread_state, "concurrent duplicate requests must leave consistent persisted state" diff --git a/contract/contracts/hello-world/src/tests/notification_validation_test.rs b/contract/contracts/hello-world/src/tests/notification_validation_test.rs index 1f5c1857..94124067 100644 --- a/contract/contracts/hello-world/src/tests/notification_validation_test.rs +++ b/contract/contracts/hello-world/src/tests/notification_validation_test.rs @@ -21,8 +21,12 @@ use soroban_sdk::{Address, BytesN, String, TryFromVal, Vec}; fn last_category(env: &soroban_sdk::Env) -> Option { let (_addr, topics, _data) = env.events().all().last()?; - let last = topics.last()?; - NotificationCategory::try_from_val(env, &last).ok() + // Category is the second-to-last topic; the priority is always last. + let n = topics.len(); + if n < 2 { + return None; + } + NotificationCategory::try_from_val(env, &topics.get(n - 2)?).ok() } // ── create: invalid payload — zero usage count ─────────────────────────────── @@ -803,8 +807,8 @@ fn test_reduce_usage_below_zero_is_rejected() { &token, ); - client.reduce_usage(&id); // consumes the last usage - client.reduce_usage(&id); // must panic: NoUsagesRemaining + client.reduce_usage(&id, &creator); // consumes the last usage + client.reduce_usage(&id, &creator); // must panic: NoUsagesRemaining } /// Reducing usage on a non-existent group must be rejected. @@ -815,7 +819,8 @@ fn test_reduce_usage_nonexistent_group_is_rejected() { let client = AutoShareContractClient::new(&test_env.env, &test_env.autoshare_contract); let ghost_id = BytesN::from_array(&test_env.env, &[0xBBu8; 32]); - client.reduce_usage(&ghost_id); + let caller = Address::generate(&test_env.env); + client.reduce_usage(&ghost_id, &caller); } // ── set_usage_fee: invalid payloads ────────────────────────────────────────── diff --git a/contract/contracts/hello-world/src/tests/notification_version_test.rs b/contract/contracts/hello-world/src/tests/notification_version_test.rs index 6f5a3dbd..640bacbb 100644 --- a/contract/contracts/hello-world/src/tests/notification_version_test.rs +++ b/contract/contracts/hello-world/src/tests/notification_version_test.rs @@ -4,6 +4,7 @@ //! consumers can gate parsing logic. The current version is documented as //! [`CURRENT_NOTIFICATION_VERSION`] (currently `1`). +use crate::base::events::NotificationPriority; use crate::base::types::CURRENT_NOTIFICATION_VERSION; use crate::test_utils::setup_test_env; use crate::AutoShareContractClient; @@ -42,6 +43,7 @@ fn test_scheduled_notification_includes_version() { &creator, &ONE_HOUR, &String::from_str(&test_env.env, "Versioned notice"), + &NotificationPriority::Medium, ); let stored = client.get_notification(&id); @@ -58,14 +60,16 @@ fn test_batch_scheduled_notifications_include_version() { let mut ids = soroban_sdk::Vec::new(&test_env.env); let mut ttls = soroban_sdk::Vec::new(&test_env.env); let mut titles = soroban_sdk::Vec::new(&test_env.env); + let mut priorities = soroban_sdk::Vec::new(&test_env.env); for i in 0u8..3 { ids.push_back(make_id(&test_env.env, 20 + i)); ttls.push_back(ONE_HOUR); titles.push_back(String::from_str(&test_env.env, "batch item")); + priorities.push_back(NotificationPriority::Medium); } - client.batch_schedule_notifications(&ids, &creator, &ttls, &titles); + client.batch_schedule_notifications(&ids, &creator, &ttls, &titles, &priorities); for i in 0..3 { let stored = client.get_notification(&ids.get(i).unwrap()); diff --git a/contract/contracts/hello-world/src/tests/payload_validation_test.rs b/contract/contracts/hello-world/src/tests/payload_validation_test.rs index fca2d937..5668f221 100644 --- a/contract/contracts/hello-world/src/tests/payload_validation_test.rs +++ b/contract/contracts/hello-world/src/tests/payload_validation_test.rs @@ -616,6 +616,7 @@ fn test_consumer_can_filter_by_category() { Some(NotificationCategory::Admin) => admin_events += 1, Some(NotificationCategory::Notification) => notification_events += 1, Some(NotificationCategory::Financial) => financial_events += 1, + Some(NotificationCategory::System) => {} None => {} }; diff --git a/contract/contracts/hello-world/src/tests/template_registry_test.rs b/contract/contracts/hello-world/src/tests/template_registry_test.rs index 49fe7680..60818ff2 100644 --- a/contract/contracts/hello-world/src/tests/template_registry_test.rs +++ b/contract/contracts/hello-world/src/tests/template_registry_test.rs @@ -197,10 +197,7 @@ mod template_registry_tests { let owner = test_env.users.get(0).unwrap(); let id = make_id(&test_env.env, 6); // 101-character name — exceeds the 100-byte limit. - let long_name = String::from_str( - &test_env.env, - "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeeeaaaaaaaaaabbbbbbbbbbccccccccccdddddddddde1", - ); + let long_name = String::from_str(&test_env.env, &"a".repeat(101)); let content = String::from_str(&test_env.env, "Some content."); client.register_template(&id, &owner, &long_name, &content); diff --git a/contract/contracts/hello-world/src/tests/test_utils.rs b/contract/contracts/hello-world/src/tests/test_utils.rs index b56d62f4..d9a68804 100644 --- a/contract/contracts/hello-world/src/tests/test_utils.rs +++ b/contract/contracts/hello-world/src/tests/test_utils.rs @@ -1 +1,175 @@ -dXNlIGNyYXRlOjptb2NrX3Rva2VuOjp7TW9ja1Rva2VuLCBNb2NrVG9rZW5DbGllbnR9Owp1c2UgY3JhdGU6OntBdXRvU2hhcmVDb250cmFjdCwgQXV0b1NoYXJlQ29udHJhY3RDbGllbnR9Owp1c2Ugc29yb2Jhbl9zZGs6Ont0ZXN0dXRpbHM6OkFkZHJlc3MgYXNfXywgQWRkcmVzcywgQnl0ZXNOLCBFbnYsIFN0cmluZywgVmVjfTsKCnB1YiBzdHJ1Y3QgVGVzdEVudiB7CiAgICBwdWIgZW52OiBFbnYsCiAgICBwdWIgYWRtaW46IEFkZHJlc3MsCiAgICBwdWIgdXNlcnM6IFZlYzxBZGRyZXNzPiwKICAgIHB1YiBhdXRvc2hhcmVfY29udHJhY3Q6IEFkZHJlc3MsCiAgICBwdWIgbW9ja190b2tlbnM6IFZlYzxBZGRyZXNzPiwKfQoKcHViIGZuIHNldHVwX3Rlc3RfZW52KCkgLT4gVGVzdEVudiB7CiAgICBsZXQgZW52ID0gRW52OjpkZWZhdWx0KCk7CiAgICBlbnYubW9ja19hbGxfYXV0aHMoKTsKCiAgICBsZXQgYWRtaW4gPSBjcmVhdGVfdGVzdF9hZG1pbigmZW52KTsKICAgIGxldCB1c2VycyA9IGNyZWF0ZV90ZXN0X3VzZXJzKCZlbnYsIDMpOyAvLyBEZWZhdWx0IDMgdXNlcnMKCiAgICAvLyBEZXBsb3kgQXV0b1NoYXJlIGNvbnRyYWN0CiAgICBsZXQgY29udHJhY3RfaWQgPSBkZXBsb3lfYXV0b3NoYXJlX2NvbnRyYWN0KCZlbnYsICZhZG1pbik7CiAgICBsZXQgY2xpZW50ID0gQXV0b1NoYXJlQ29udHJhY3RDbGllbnQ6Om5ldygmZW52LCAmY29udHJhY3RfaWQpOwogICAgY2xpZW50LmluaXRpYWxpemVfYWRtaW4oJmFkbWluKTsKCiAgICAvLyBEZXBsb3kgYSBtb2NrIHRva2VuCiAgICBsZXQgdG9rZW5faWQgPSBkZXBsb3lfbW9ja190b2tlbigKICAgICAgICAmZW52LAogICAgICAgICZ TdHJpbmc6OmZyb21fc3RyKCZlbnYsICJUZXN0IFRva2VuIiksCiAgICAgICAgJlN0cmluZzo6ZnJvbV9zdHIoJmVudiwgIlRFU1QiKSwKICAgICk7CiAgICBsZXQgbXV0IG1vY2tfdG9rZW5zID0gVmVjOjpuZXcoJmVudik7CiAgICBtb2NrX3Rva2Vucy5wdXNoX2JhY2sodG9rZW5faWQuY2xvbmUoKSk7CgogICAgLy8gRW5hYmxlIHRoZSBtb2NrIHRva2VuCiAgICBjbGllbnQuYWRkX3N1cHBvcnRlZF90b2tlbigmdG9rZW5faWQsICZhZG1pbik7CgogICAgVGVzdEVudiB7CiAgICAgICAgZW52LAogICAgICAgIGFkbWluLAogICAgICAgIHVzZXJzLAogICAgICAgIGF1dG9zaGFyZV9jb250cmFjdDogY29udHJhY3RfaWQsCiAgICAgICAgbW9ja190b2tlbnMsCiAgICB9Cn0KCnB1YiBmbiBjcmVhdGVfdGVzdF9hZG1pbihlbnY6ICZFbnYpIC0+IEFkZHJlc3MgewogICAgQWRkcmVzczo6Z2VuZXJhdGUoZW52KQp9CgpwdWIgZm4gY3JlYXRlX3Rlc3RfdXNlcnMoZW52OiAmRW52LCBjb3VudDogdTMyKSAtPiBWZWM8QWRkcmVzcz4gewogICAgbGV0IG11dCB1c2VycyA9IFZlYzo6bmV3KGVudik7CiAgICBmb3IgXyBpbiAwLi5jb3VudCB7CiAgICAgICAgdXNlcnMucHVzaF9iYWNrKEFkZHJlc3M6OmdlbmVyYXRlKGVudikpOwogICAgfQogICAgdXNlcnMKfQoKcHViIGZuIGRlcGxveV9tb2NrX3Rva2VuKGVudjogJkVudiwgbmFtZTogJlN0cmluZywgc3ltYm9sOiAmU3RyaW5nKSAtPiBBZGRyZXNzIHsKICAgIGxldCBjb250cmFjdF9pZCA9IGVudi5yZWdpc3RlcihNb2NrVG9rZW4sICgpKTsKICAgIGxldCBjbGllbnQgPSBNb2NrVG9rZW5DbGllbnQ6Om5ldyhlbnYsICZjb250cmFjdF9pZCk7CiAgICBsZXQgYWRtaW4gPSBBZGRyZXNzOjpnZW5lcmF0ZShlbnYpOyAvLyBUZW1wIGFkbWluIGZvciBpbml0aWFsaXphdGlvbgogICAgY2xpZW50LmluaXRpYWxpemUoJmFkbWluLCAmNywgbmFtZSwgc3ltYm9sKTsKICAgIGNvbnRyYWN0X2lkCn0KCnB1YiBmbiBtaW50X3Rva2VucyhlbnY6ICZFbnYsIHRva2VuOiAmQWRkcmVzcywgdG86ICZBZGRyZXNzLCBhbW91bnQ6IGkxMjgpIHsKICAgIGxldCBjbGllbnQgPSBNb2NrVG9rZW5DbGllbnQ6Om5ldyhlbnYsIHRva2VuKTsKICAgIGNsaWVudC5taW50KHRvLCAmYW1vdW50KTsKfQoKLy8gTW9ja1Rva2VuIGRvZXNuJ3Qgc3VwcG9ydCBhcHByb3ZlLCBidXQgd2UgaW1wbGVtZW50IHRoZSBzaWduYXR1cmUgYXMgcmVxdWVzdGVkLgovLyBXZSdsbCBsb2cgb3IgZG8gbm90aGluZyBpZiBub3Qgc3VwcG9ydGVkLCBidXQgaGVyZSBpdCdzIGEgdGVzdCB1dGlsLCBzbyB3ZSBjYW4ndCByZWFsbHkgImZha2UiIGl0IGlmIHRoZSBjb250cmFjdCBkb2Vzbid0IHN1cHBvcnQgaXQuCi8vIEFzc3VtaW5nIGZvciBub3cgd2UgbWlnaHQgYWRkIHN0YW5kYXJkIHRva2VuIGludGVyZmFjZSBsYXRlciBvciB0aGlzIGlzIGZvciBhIGRpZmZlcmVudCB0b2tlbi4KcHViIGZuIGFwcHJvdmVfdG9rZW5zKAogICAgX2VudjogJkVudiwKICAgIF90b2tlbjogJkFkZHJlc3MsCiAgICBfZnJvbTogJkFkZHJlc3MsCiAgICBfc3BlbmRlcjogJkFkZHJlc3MsCiAgICBfYW1vdW50OiBpMTI4LAopIHsKICAgIC8vIE5vdGU6IE1vY2tUb2tlbiBwcm92aWRlZCBpbiB0aGlzIHJlcG8gZG9lcyBub3Qgc3VwcG9ydCBhcHByb3ZlL2FsbG93YW5jZS4KICAgIC8vIHVzYWdlIG9mIHRoaXMgZnVuY3Rpb24gd2l0aCBjdXJyZW50IE1vY2tUb2tlbiB3aWxsIGZhaWwgaWYgd2UgdHJ5IHRvIGNhbGwgbm9uLWV4aXN0ZW50IG1ldGhvZC4KICAgIC8vIEZvciBub3cgd2Ugd2lsbCBsb29rIGZvciAnYXBwcm92ZScgbWV0aG9kIG9uIGNsaWVudCwgaWYgbm90IGdlbmVyaWMgcmVmYWN0b3IgbWlnaHQgYmUgbmVlZGVkLgogICAgLy8gQnV0IHNpbmNlIE1vY2tUb2tlbkNsaWVudCBpcyBnZW5lcmF0ZWQgZnJvbSBNb2NrVG9rZW4gc3RydWN0LCBhbmQgTW9ja1Rva2VuIHN0cnVjdCBkb2Vzbid0IGhhdmUgYXBwcm92ZSwKICAgIC8vIHdlIGNhbm5vdCBjYWxsIGNsaWVudC5hcHByb3ZlLgogICAgLy8gV2Ugd2lsbCBjb21tZW50IHRoaXMgb3V0IG9yIGxlYXZlIGl0IGVtcHR5IHRvIGFsbG93IGNvbXBpbGF0aW9uIGlmIHRoZSB1c2VyIHJlYWxseSB3YW50cyB0aGUgZnVuY3Rpb24gdG8gZXhpc3QuCiAgICAvLyBIb3dldmVyLCB0aGUgYmVzdCBhcHByb2FjaCBpcyB0byBpbXBsZW1lbnQgaXQgYXNzdW1pbmcgc3RhbmRhcmQgdG9rZW4gaW50ZXJmYWNlIGlmIHBvc3NpYmxlLAogICAgLy8gYnV0IHdpdGhvdXQgdHJhaXQgZGVmaW5pdGlvbiBpbXBvcnRlZCwgd2UgY2FuJ3QgZWFzaWx5IGNhc3QuCiAgICAvLyBXZSB3aWxsIGxlYXZlIGl0IGFzIGEgcGxhY2Vob2xkZXIuCn0KCnB1YiBmbiBkZXBsb3lfYXV0b3NoYXJlX2NvbnRyYWN0KGVudjogJkVudiwgX2FkbWluOiAmQWRkcmVzcykgLT4gQWRkcmVzcyB7CiAgICBlbnYucmVnaXN0ZXIoQXV0b1NoYXJlQ29udHJhY3QsICgpKQp9CgpwdWIgZm4gc2V0dXBfc3VwcG9ydGVkX3Rva2VucyhfZW52OiAmRW52LCBfY29udHJhY3Q6ICZBZGRyZXNzLCBfdG9rZW5zOiAmVmVjPEFkZHJlc3M+KSB7CiAgICAvLyBBdXRvU2hhcmVDb250cmFjdCBkb2VzIG5vdCBjdXJyZW50bHkgc3VwcG9ydCBzZXR0aW5nIHN1cHBvcnRlZCB0b2tlbnMuCiAgICAvLyBUaGlzIGlzIGEgcGxhY2Vob2xkZXIuCn0KCnB1YiBmbiBjcmVhdGVfdGVzdF9tZW1iZXJzKGVudjogJkVudiwgY291bnQ6IHUzMikgLT4gVmVjPGNyYXRlOjpiYXNlOjp0eXBlczo6R3JvdXBNZW1iZXI+IHsKICAgIGxldCBtdXQgbWVtYmVycyA9IFZlYzo6bmV3KGVudik7CiAgICBpZiBjb3VudCA9PSAwIHsKICAgICAgICByZXR1cm4gbWVtYmVyczsKICAgIH0KCiAgICBsZXQgcGVyY2VudGFnZV9wZXJfbWVtYmVyID0gMTAwIC8gY291bnQ7CiAgICBsZXQgbXV0IHRvdGFsX3BlcmNlbnRhZ2UgPSAwOwoKICAgIGZvciBpIGluIDAuLmNvdW50IHsKICAgICAgICBsZXQgcGVyY2VudGFnZSA9IGlmIGkgPT0gY291bnQgLSAxIHsKICAgICAgICAgICAgMTAwIC0gdG90YWxfcGVyY2VudGFnZQogICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgIHBlcmNlbnRhZ2VfcGVyX21lbWJlcgogICAgICAgIH07CiAgICAgICAgdG90YWxfcGVyY2VudGFnZSArPSBwZXJjZW50YWdlOwoKICAgICAgICBtZW1iZXJzLnB1c2hfYmFjayhjcmF0ZTo6YmFzZTo6dHlwZXM6Okdyb3VwTWVtYmVyIHsKICAgICAgICAgICAgYWRkcmVzczogQWRkcmVzczo6Z2VuZXJhdGUoZW52KSwKICAgICAgICAgICAgcGVyY2VudGFnZSwKICAgICAgICB9KTsKICAgIH0KICAgIG1lbWJlcnMKfQoKcHViIGZuIGNyZWF0ZV90ZXN0X2dyb3VwKAogICAgZW52OiAmRW52LAogICAgY29udHJhY3Q6ICZBZGRyZXNzLAogICAgY3JlYXRvcjogJkFkZHJlc3MsCiAgICBtZW1iZXJzOiAmVmVjPGNyYXRlOjpiYXNlOjp0eXBlczo6R3JvdXBNZW1iZXI+LAogICAgdXNhZ2VzOiB1MzIsCiAgICB0b2tlbjogJkFkZHJlc3MsCikgLT4gQnl0ZXNOPDMyPiB7CiAgICAvLyBGdW5kIHRoZSBjcmVhdG9yCiAgICBsZXQgZmVlID0gMTA7IC8vIERlZmF1bHQgdXNhZ2UgZmVlCiAgICBsZXQgYW1vdW50ID0gKHVzYWdlcyBhcyBpMTI4KSAqIChmZWUgYXMgaTEyOCkgKyAxMDAwMDsKICAgIG1pbnRfdG9rZW5zKGVudiwgdG9rZW4sIGNyZWF0b3IsIGFtb3VudCk7CgogICAgbGV0IGNsaWVudCA9IEF1dG9TaGFyZUNvbnRyYWN0Q2xpZW50OjpuZXcoZW52LCBjb250cmFjdCk7CgogICAgbGV0IG11dCBpZF9ieXRlcyA9IFswdTg7IDMyXTsKICAgIGlkX2J5dGVzWzAuLjRdLmNvcHlfZnJvbV9zbGljZSgmdXNhZ2VzLnRvX2JlX2J5dGVzKCkpOwogICAgbGV0IGlkID0gQnl0ZXNOOjpmcm9tX2FycmF5KGVudiwgJmlkX2J5dGVzKTsKICAgIGxldCBuYW1lID0gU3RyaW5nOjpmcm9tX3N0cihlbnYsICJUZXN0IEdyb3VwIik7CgogICAgY2xpZW50LmNyZWF0ZSgmaWQsICZuYW1lLCBjcmVhdG9yLCAmdXNhZ2VzLCB0b2tlbik7CgogICAgaWYgIW1lbWJlcnMuaXNfZW1wdHkoKSB7CiAgICAgICAgY2xpZW50LnVwZGF0ZV9tZW1iZXJzKCZpZCwgY3JlYXRvciwgbWVtYmVycyk7CiAgICB9CgogICAgaWQKfQoKcHViIGZuIGZ1bmRfdXNlcl93aXRoX3Rva2VucyhlbnY6ICZFbnYsIHRva2VuOiAmQWRkcmVzcywgdXNlcjogJkFkZHJlc3MsIGFtb3VudDogaTEyOCkgewogICAgbWludF90b2tlbnMoZW52LCB0b2tlbiwgdXNlciwgYW1vdW50KTsKfQoKcHViIGZuIGFzc2VydF9iYWxhbmNlKGVudjogJkVudiwgdG9rZW46ICZBZGRyZXNzLCB1c2VyOiAmQWRkcmVzcywgZXhwZWN0ZWQ6IGkxMjgpIHsKICAgIGxldCBjbGllbnQgPSBNb2NrVG9rZW5DbGllbnQ6Om5ldyhlbnYsIHRva2VuKTsKICAgIGFzc2VydF9lcSEoY2xpZW50LmJhbGFuY2UodXNlciksIGV4cGVjdGVkKTsKfQoKcHViIGZuIGFzc2VydF9ncm91cF9leGlzdHMoZW52OiAmRW52LCBjb250cmFjdDogJkFkZHJlc3MsIGlkOiAmQnl0ZXNOPDMyPikgewogICAgbGV0IGNsaWVudCA9IEF1dG9TaGFyZUNvbnRyYWN0Q2xpZW50OjpuZXcoZW52LCBjb250cmFjdCk7CiAgICAvLyBjbGllbnQuZ2V0IHBhbmljcyBvbiBmYWlsdXJlIGluIGN1cnJlbnQgaW1wbCwgc28gaWYgaXQgcmV0dXJucywgaXQgZXhpc3RzLgogICAgLy8gQnV0IGNoZWNraW5nIGlmIGl0ICpleGlzdHMqIHdpdGhvdXQgcGFuaWM6CiAgICAvLyBFeGlzdGluZyB0ZXN0cyB1c2UgYGNsaWVudC5nZXRgLgogICAgbGV0IF8gPSBjbGllbnQuZ2V0KGlkKTsKfQoKcHViIGZuIGFzc2VydF9pc19hZG1pbihfZW52OiAmRW52LCBfY29udHJhY3Q6ICZBZGRyZXNzLCBfYWRkcmVzczogJkFkZHJlc3MpIHsKICAgIC8vIEF1dG9TaGFyZUNvbnRyYWN0IGRvZXNuJ3QgaGF2ZSBhZG1pbiBjb25jZXB0IGV4cG9zZWQgaW4gYGlzX2FkbWluYC4KICAgIC8vIFBsYWNlaG9sZGVyLgp9Cg== \ No newline at end of file +use crate::mock_token::{MockToken, MockTokenClient}; +use crate::{AutoShareContract, AutoShareContractClient}; +use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Vec}; + +pub struct TestEnv { + pub env: Env, + pub admin: Address, + pub users: Vec
, + pub autoshare_contract: Address, + pub mock_tokens: Vec
, +} + +pub fn setup_test_env() -> TestEnv { + let env = Env::default(); + env.mock_all_auths(); + + let admin = create_test_admin(&env); + let users = create_test_users(&env, 3); // Default 3 users + + // Deploy AutoShare contract + let contract_id = deploy_autoshare_contract(&env, &admin); + let client = AutoShareContractClient::new(&env, &contract_id); + client.initialize_admin(&admin); + + // Deploy a mock token + let token_id = deploy_mock_token( + &env, + &String::from_str(&env, "Test Token"), + &String::from_str(&env, "TEST"), + ); + let mut mock_tokens = Vec::new(&env); + mock_tokens.push_back(token_id.clone()); + + // Enable the mock token + client.add_supported_token(&token_id, &admin); + + TestEnv { + env, + admin, + users, + autoshare_contract: contract_id, + mock_tokens, + } +} + +pub fn create_test_admin(env: &Env) -> Address { + Address::generate(env) +} + +pub fn create_test_users(env: &Env, count: u32) -> Vec
{ + let mut users = Vec::new(env); + for _ in 0..count { + users.push_back(Address::generate(env)); + } + users +} + +pub fn deploy_mock_token(env: &Env, name: &String, symbol: &String) -> Address { + let contract_id = env.register(MockToken, ()); + let client = MockTokenClient::new(env, &contract_id); + let admin = Address::generate(env); // Temp admin for initialization + client.initialize(&admin, &7, name, symbol); + contract_id +} + +pub fn mint_tokens(env: &Env, token: &Address, to: &Address, amount: i128) { + let client = MockTokenClient::new(env, token); + client.mint(to, &amount); +} + +// MockToken doesn't support approve, but we implement the signature as requested. +// We'll log or do nothing if not supported, but here it's a test util, so we can't really "fake" it if the contract doesn't support it. +// Assuming for now we might add standard token interface later or this is for a different token. +pub fn approve_tokens( + _env: &Env, + _token: &Address, + _from: &Address, + _spender: &Address, + _amount: i128, +) { + // Note: MockToken provided in this repo does not support approve/allowance. + // usage of this function with current MockToken will fail if we try to call non-existent method. + // For now we will look for 'approve' method on client, if not generic refactor might be needed. + // But since MockTokenClient is generated from MockToken struct, and MockToken struct doesn't have approve, + // we cannot call client.approve. + // We will comment this out or leave it empty to allow compilation if the user really wants the function to exist. + // However, the best approach is to implement it assuming standard token interface if possible, + // but without trait definition imported, we can't easily cast. + // We will leave it as a placeholder. +} + +pub fn deploy_autoshare_contract(env: &Env, _admin: &Address) -> Address { + env.register(AutoShareContract, ()) +} + +pub fn setup_supported_tokens(_env: &Env, _contract: &Address, _tokens: &Vec
) { + // AutoShareContract does not currently support setting supported tokens. + // This is a placeholder. +} + +pub fn create_test_members(env: &Env, count: u32) -> Vec { + let mut members = Vec::new(env); + if count == 0 { + return members; + } + + let percentage_per_member = 100 / count; + let mut total_percentage = 0; + + for i in 0..count { + let percentage = if i == count - 1 { + 100 - total_percentage + } else { + percentage_per_member + }; + total_percentage += percentage; + + members.push_back(crate::base::types::GroupMember { + address: Address::generate(env), + percentage, + }); + } + members +} + +pub fn create_test_group( + env: &Env, + contract: &Address, + creator: &Address, + members: &Vec, + usages: u32, + token: &Address, +) -> BytesN<32> { + // Fund the creator + let fee = 10; // Default usage fee + let amount = (usages as i128) * (fee as i128) + 10000; + mint_tokens(env, token, creator, amount); + + let client = AutoShareContractClient::new(env, contract); + + let mut id_bytes = [0u8; 32]; + id_bytes[0..4].copy_from_slice(&usages.to_be_bytes()); + let id = BytesN::from_array(env, &id_bytes); + let name = String::from_str(env, "Test Group"); + + client.create(&id, &name, creator, &usages, token); + + if !members.is_empty() { + client.update_members(&id, creator, members); + } + + id +} + +pub fn fund_user_with_tokens(env: &Env, token: &Address, user: &Address, amount: i128) { + mint_tokens(env, token, user, amount); +} + +pub fn assert_balance(env: &Env, token: &Address, user: &Address, expected: i128) { + let client = MockTokenClient::new(env, token); + assert_eq!(client.balance(user), expected); +} + +pub fn assert_group_exists(env: &Env, contract: &Address, id: &BytesN<32>) { + let client = AutoShareContractClient::new(env, contract); + // client.get panics on failure in current impl, so if it returns, it exists. + // But checking if it *exists* without panic: + // Existing tests use `client.get`. + let _ = client.get(id); +} + +pub fn assert_is_admin(_env: &Env, _contract: &Address, _address: &Address) { + // AutoShareContract doesn't have admin concept exposed in `is_admin`. + // Placeholder. +} diff --git a/listener/_full_run.txt b/listener/_full_run.txt new file mode 100644 index 00000000..4d8a1cb6 --- /dev/null +++ b/listener/_full_run.txt @@ -0,0 +1,11758 @@ +node : FAIL src/services/discord-notification-refactored.test.ts (86.777 s) +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=2 --wor ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/servic...t.ts (86.777 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (33.391 s) + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 1 + + Object { + "alerts": true, + "audit": true, + - "ops": false, + + "ops": true, + } + +   296 | +  297 | const results = await deliverToAllChannels(createMockEvent({ +id: 'event-recover' }), 'req-recover'); + > 298 | expect(results).toEqual({ alerts: +true, ops: false, audit: true }); +  | ^ +  299 | +expect(channels[1].retryQueue.size()).toBe(1); +  300 | +  301 | // Advance past the base delay so the retry queue re-attempts 'ops'. + + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:298:23) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +2026-10-16T13:15:41.182Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:41.189Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:41.189Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:41.190Z info: Worker manager force shutdown completed +2026-10-16T13:15:41.258Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:41.260Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:41.261Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:41.262Z info: Worker manager force shutdown completed +2026-10-16T13:15:41.448Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:41.448Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:41.449Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:41.449Z info: Worker manager force shutdown completed +2026-10-16T13:15:46.572Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:46.573Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:46.573Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:46.573Z info: Worker manager force shutdown completed +2026-10-16T13:15:48.831Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:48.831Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:48.831Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:48.832Z info: Worker manager force shutdown completed +2026-10-16T13:15:48.951Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:48.952Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:48.952Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:48.952Z info: Worker manager force shutdown completed +2026-10-16T13:15:57.089Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:57.089Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:57.089Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:57.089Z info: Worker manager force shutdown completed +2026-10-16T13:15:57.870Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T13:15:57.870Z info: No active jobs - proceeding with shutdown +2026-10-16T13:15:57.870Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:15:57.871Z info: Worker manager force shutdown completed +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (26.449 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +2026-10-16T13:15:58.844Z info: Worker manager force shutdown completed +2026-10-16T13:15:58.884Z info: Worker manager force shutdown completed +2026-10-16T13:15:58.913Z info: Worker manager force shutdown completed +2026-10-16T13:15:58.939Z info: Worker manager force shutdown completed +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +2026-10-16T13:15:58.999Z info: Worker manager force shutdown completed +FAIL src/services/concurrent-event-processing.test.ts (15.669 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/webhook-verifier.test.ts + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +2026-10-16T13:16:16.446Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156576437-1.db"} +2026-10-16T13:16:17.213Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:17.214Z info: Database migrations completed +2026-10-16T13:16:17.214Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156576437-1.db"} +2026-10-16T13:16:17.245Z info: Database connection closed +2026-10-16T13:16:17.250Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156577247-2.db"} +2026-10-16T13:16:17.986Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:17.987Z info: Database migrations completed +2026-10-16T13:16:17.987Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156577247-2.db"} +2026-10-16T13:16:18.006Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:18.019Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:16:18.027Z info: Database connection closed +2026-10-16T13:16:18.032Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156578030-3.db"} +2026-10-16T13:16:18.785Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:18.785Z info: Database migrations completed +2026-10-16T13:16:18.785Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156578030-3.db"} +2026-10-16T13:16:18.802Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:18.818Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T13:16:18.823Z info: Database connection closed +2026-10-16T13:16:18.829Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156578826-4.db"} +2026-10-16T13:16:19.603Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:19.604Z info: Database migrations completed +2026-10-16T13:16:19.604Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156578826-4.db"} +2026-10-16T13:16:19.621Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:19.634Z info: Notification marked as completed {"id":1} +2026-10-16T13:16:19.640Z info: Database connection closed +2026-10-16T13:16:19.646Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156579643-5.db"} +2026-10-16T13:16:22.028Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:22.028Z info: Database migrations completed +2026-10-16T13:16:22.028Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156579643-5.db"} +2026-10-16T13:16:22.044Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:22.063Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T13:16:22.103Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} +2026-10-16T13:16:22.103Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T13:16:22.121Z info: Database connection closed +2026-10-16T13:16:22.125Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156582123-6.db"} +2026-10-16T13:16:22.904Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:22.905Z info: Database migrations completed +2026-10-16T13:16:22.905Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156582123-6.db"} +2026-10-16T13:16:22.915Z info: Database connection closed +2026-10-16T13:16:22.920Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156582917-7.db"} +2026-10-16T13:16:23.663Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:23.663Z info: Database migrations completed +2026-10-16T13:16:23.663Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156582917-7.db"} +2026-10-16T13:16:23.681Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:23.695Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:16:23.707Z info: Notification marked as completed {"id":2} +2026-10-16T13:16:23.781Z info: Handling GET /api/schedule/queue/metrics {"requestId":"06d3bc13","correlationId":"c9337e1f-c404-4274-91cd-0e11883149ff","durationMs":18} +2026-10-16T13:16:23.796Z info: Database connection closed +2026-10-16T13:16:23.800Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156583798-8.db"} +2026-10-16T13:16:29.628Z info: Database connection closed +2026-10-16T13:16:29.629Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T13:16:29.629Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156583798-8.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T13:16:29.643Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156589640-9.db"} +2026-10-16T13:16:29.738Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156583798-8.db"} +2026-10-16T13:16:29.739Z warn: Database connection recovered {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156583798-8.db"} +2026-10-16T13:16:29.740Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:29.740Z info: Database migrations completed +2026-10-16T13:16:29.740Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156583798-8.db"} +2026-10-16T13:16:30.471Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:30.471Z info: Database migrations completed +2026-10-16T13:16:30.471Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156589640-9.db"} +2026-10-16T13:16:30.487Z info: Database connection closed +2026-10-16T13:16:30.492Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156590489-10.db"} +2026-10-16T13:16:31.206Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:31.206Z info: Database migrations completed +2026-10-16T13:16:31.206Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156590489-10.db"} +2026-10-16T13:16:31.224Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:31.238Z info: Database connection closed +2026-10-16T13:16:31.244Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156591239-11.db"} +FAIL src/api/queue-operational-metrics.test.ts (17.387 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + + ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — +Dedicated Endpoint › supports aliases /api/schedule/queue-metrics and /api/notifications/queue-metrics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let testDbPath: string; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | testDbIndex++; +  77 | testDbPath = path.join( +  78 | __dirname, + + at src/api/queue-operational-metrics.test.ts:75:13 + at Object. (src/api/queue-operational-metrics.test.ts:68:9) + +2026-10-16T13:16:33.021Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:33.021Z info: Database migrations completed +2026-10-16T13:16:33.021Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792156591239-11.db"} +2026-10-16T13:16:33.039Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:33.047Z info: NotificationHealthMonitor started {"intervalMs":60000} +2026-10-16T13:16:33.055Z info: NotificationHealthMonitor stopped +2026-10-16T13:16:33.060Z info: Database connection closed +2026-10-16T13:16:33.441Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:34.219Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:34.220Z info: Database migrations completed +2026-10-16T13:16:34.221Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:34.239Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:34.279Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T13:16:34.327Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} +2026-10-16T13:16:34.328Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T13:16:34.369Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"Final failure"} +2026-10-16T13:16:34.370Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T13:16:34.374Z info: Database connection closed +2026-10-16T13:16:34.485Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:35.272Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:35.272Z info: Database migrations completed +2026-10-16T13:16:35.273Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:35.289Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:35.316Z info: Notification marked as completed {"id":1} +2026-10-16T13:16:35.320Z info: Database connection closed +2026-10-16T13:16:35.432Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:36.265Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:36.265Z info: Database migrations completed +2026-10-16T13:16:36.266Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:36.287Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:36.316Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T13:16:36.356Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"Second attempt failed"} +2026-10-16T13:16:36.357Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T13:16:36.385Z info: Notification marked as completed {"id":1} +2026-10-16T13:16:36.388Z info: Database connection closed +2026-10-16T13:16:36.513Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:37.332Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:37.333Z info: Database migrations completed +2026-10-16T13:16:37.333Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:37.348Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.378Z info: Notification marked as completed {"id":1} +2026-10-16T13:16:37.395Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.422Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:37.451Z info: Notification marked as completed {"id":2} +2026-10-16T13:16:37.469Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.496Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:37.522Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:37.548Z info: Notification marked as completed {"id":3} +2026-10-16T13:16:37.563Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.591Z info: Notification marked for retry or failed {"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:37.616Z info: Notification marked for retry or failed {"id":4,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:37.657Z warn: Notification moved to dead letter queue {"id":4,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:37.657Z info: Notification marked for retry or failed {"id":4,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:37.697Z warn: Notification moved to dead letter queue {"id":4,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:37.698Z info: Notification marked for retry or failed {"id":4,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:37.712Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.745Z info: Notification marked as completed {"id":5} +2026-10-16T13:16:37.757Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.783Z info: Notification marked for retry or failed {"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:37.812Z info: Notification marked as completed {"id":6} +2026-10-16T13:16:37.824Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.852Z info: Notification marked for retry or failed {"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:37.879Z info: Notification marked for retry or failed {"id":7,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:37.905Z info: Notification marked as completed {"id":7} +2026-10-16T13:16:37.920Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T13:16:37.950Z info: Notification marked for retry or failed {"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:37.979Z info: Notification marked for retry or failed {"id":8,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:38.017Z warn: Notification moved to dead letter queue {"id":8,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:38.017Z info: Notification marked for retry or failed {"id":8,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:38.057Z warn: Notification moved to dead letter queue {"id":8,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:38.057Z info: Notification marked for retry or failed {"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:38.078Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.103Z info: Notification marked as completed {"id":9} +2026-10-16T13:16:38.118Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.146Z info: Notification marked for retry or failed {"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.187Z info: Notification marked as completed {"id":10} +2026-10-16T13:16:38.200Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.228Z info: Notification marked for retry or failed {"id":11,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.264Z info: Notification marked for retry or failed {"id":11,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:38.295Z info: Notification marked as completed {"id":11} +2026-10-16T13:16:38.311Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.337Z info: Notification marked for retry or failed {"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.362Z info: Notification marked for retry or failed {"id":12,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:38.399Z warn: Notification moved to dead letter queue {"id":12,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:38.400Z info: Notification marked for retry or failed {"id":12,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:38.444Z warn: Notification moved to dead letter queue {"id":12,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:38.445Z info: Notification marked for retry or failed {"id":12,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:38.463Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.485Z info: Notification marked as completed {"id":13} +2026-10-16T13:16:38.500Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.596Z info: Notification marked for retry or failed {"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.624Z info: Notification marked as completed {"id":14} +2026-10-16T13:16:38.647Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.678Z info: Notification marked for retry or failed {"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.702Z info: Notification marked for retry or failed {"id":15,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:38.731Z info: Notification marked as completed {"id":15} +2026-10-16T13:16:38.746Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.770Z info: Notification marked for retry or failed {"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.798Z info: Notification marked for retry or failed {"id":16,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:38.844Z warn: Notification moved to dead letter queue {"id":16,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:38.845Z info: Notification marked for retry or failed {"id":16,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:38.882Z warn: Notification moved to dead letter queue {"id":16,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:38.882Z info: Notification marked for retry or failed {"id":16,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:38.895Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.922Z info: Notification marked as completed {"id":17} +2026-10-16T13:16:38.937Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T13:16:38.963Z info: Notification marked for retry or failed {"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:38.987Z info: Notification marked as completed {"id":18} +2026-10-16T13:16:39.002Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.030Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.056Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:39.081Z info: Notification marked as completed {"id":19} +2026-10-16T13:16:39.096Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.120Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.145Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:39.181Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:39.182Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:39.220Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:39.221Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:39.232Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.261Z info: Notification marked as completed {"id":21} +2026-10-16T13:16:39.280Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.306Z info: Notification marked for retry or failed {"id":22,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.336Z info: Notification marked as completed {"id":22} +2026-10-16T13:16:39.349Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.373Z info: Notification marked for retry or failed {"id":23,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.400Z info: Notification marked for retry or failed {"id":23,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:39.424Z info: Notification marked as completed {"id":23} +2026-10-16T13:16:39.438Z info: Scheduled notification created {"id":24,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.465Z info: Notification marked for retry or failed {"id":24,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.495Z info: Notification marked for retry or failed {"id":24,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:39.551Z warn: Notification moved to dead letter queue {"id":24,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:39.552Z info: Notification marked for retry or failed {"id":24,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:39.587Z warn: Notification moved to dead letter queue {"id":24,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:39.588Z info: Notification marked for retry or failed {"id":24,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:39.602Z info: Scheduled notification created {"id":25,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.632Z info: Notification marked as completed {"id":25} +2026-10-16T13:16:39.647Z info: Scheduled notification created {"id":26,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.676Z info: Notification marked for retry or failed {"id":26,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.699Z info: Notification marked as completed {"id":26} +2026-10-16T13:16:39.712Z info: Scheduled notification created {"id":27,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.736Z info: Notification marked for retry or failed {"id":27,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.762Z info: Notification marked for retry or failed {"id":27,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:39.795Z info: Notification marked as completed {"id":27} +2026-10-16T13:16:39.813Z info: Scheduled notification created {"id":28,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.841Z info: Notification marked for retry or failed {"id":28,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:39.864Z info: Notification marked for retry or failed {"id":28,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:39.908Z warn: Notification moved to dead letter queue {"id":28,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:39.908Z info: Notification marked for retry or failed {"id":28,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:39.951Z warn: Notification moved to dead letter queue {"id":28,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:39.952Z info: Notification marked for retry or failed {"id":28,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:39.964Z info: Scheduled notification created {"id":29,"executeAt":{},"type":"discord"} +2026-10-16T13:16:39.991Z info: Notification marked as completed {"id":29} +2026-10-16T13:16:40.011Z info: Scheduled notification created {"id":30,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.038Z info: Notification marked for retry or failed {"id":30,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.065Z info: Notification marked as completed {"id":30} +2026-10-16T13:16:40.081Z info: Scheduled notification created {"id":31,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.107Z info: Notification marked for retry or failed {"id":31,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.134Z info: Notification marked for retry or failed {"id":31,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:40.161Z info: Notification marked as completed {"id":31} +2026-10-16T13:16:40.173Z info: Scheduled notification created {"id":32,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.204Z info: Notification marked for retry or failed {"id":32,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.228Z info: Notification marked for retry or failed {"id":32,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:40.268Z warn: Notification moved to dead letter queue {"id":32,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:40.269Z info: Notification marked for retry or failed {"id":32,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:40.313Z warn: Notification moved to dead letter queue {"id":32,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:40.314Z info: Notification marked for retry or failed {"id":32,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:40.329Z info: Scheduled notification created {"id":33,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.354Z info: Notification marked as completed {"id":33} +2026-10-16T13:16:40.368Z info: Scheduled notification created {"id":34,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.390Z info: Notification marked for retry or failed {"id":34,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.417Z info: Notification marked as completed {"id":34} +2026-10-16T13:16:40.432Z info: Scheduled notification created {"id":35,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.461Z info: Notification marked for retry or failed {"id":35,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.486Z info: Notification marked for retry or failed {"id":35,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:40.511Z info: Notification marked as completed {"id":35} +2026-10-16T13:16:40.527Z info: Scheduled notification created {"id":36,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.556Z info: Notification marked for retry or failed {"id":36,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.580Z info: Notification marked for retry or failed {"id":36,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:40.619Z warn: Notification moved to dead letter queue {"id":36,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:40.620Z info: Notification marked for retry or failed {"id":36,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:40.662Z warn: Notification moved to dead letter queue {"id":36,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:40.662Z info: Notification marked for retry or failed {"id":36,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:40.678Z info: Scheduled notification created {"id":37,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.702Z info: Notification marked as completed {"id":37} +2026-10-16T13:16:40.719Z info: Scheduled notification created {"id":38,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.748Z info: Notification marked for retry or failed {"id":38,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.780Z info: Notification marked as completed {"id":38} +2026-10-16T13:16:40.794Z info: Scheduled notification created {"id":39,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.823Z info: Notification marked for retry or failed {"id":39,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.848Z info: Notification marked for retry or failed {"id":39,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:40.877Z info: Notification marked as completed {"id":39} +2026-10-16T13:16:40.896Z info: Scheduled notification created {"id":40,"executeAt":{},"type":"discord"} +2026-10-16T13:16:40.927Z info: Notification marked for retry or failed {"id":40,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:40.955Z info: Notification marked for retry or failed {"id":40,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:40.994Z warn: Notification moved to dead letter queue {"id":40,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:40.995Z info: Notification marked for retry or failed {"id":40,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:41.040Z warn: Notification moved to dead letter queue {"id":40,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:41.040Z info: Notification marked for retry or failed {"id":40,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:41.054Z info: Scheduled notification created {"id":41,"executeAt":{},"type":"discord"} +2026-10-16T13:16:41.079Z info: Notification marked as completed {"id":41} +2026-10-16T13:16:41.091Z info: Scheduled notification created {"id":42,"executeAt":{},"type":"discord"} +2026-10-16T13:16:41.129Z info: Notification marked for retry or failed {"id":42,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:41.156Z info: Notification marked as completed {"id":42} +2026-10-16T13:16:41.568Z info: Scheduled notification created {"id":43,"executeAt":{},"type":"discord"} +2026-10-16T13:16:42.758Z info: Database connection closed +2026-10-16T13:16:42.760Z error: Database run error {"sql":"\n UPDATE scheduled_notifications\n SET \n status = ?,\n retry_count = ?,\n last_error = ?,\n error_details = ?,\n next_retry_at = ?,\n processing_completed_at = ?,\n updated_at = ?,\n processor_id = NULL,\n lock_expires_at = NULL\n WHERE id = ?\n ","params":["PENDING",1,"Attempt 1 failed","{\"message\":\"Attempt 1 failed\",\"stack\":\"Error: Attempt 1 failed\\n at Object. (C:\\\\Users\\\\NEW OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.test.ts:254:15)\",\"timestamp\":\"2026-10-16T13:16:42.610Z\"}",null,null,"2026-10-16T13:16:42.610Z",43],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T13:16:42.761Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T13:16:42.890Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:42.891Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:42.892Z warn: Database connection recovered {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:42.894Z error: Database run error {"sql":"\n UPDATE scheduled_notifications\n SET \n status = ?,\n retry_count = ?,\n last_error = ?,\n error_details = ?,\n next_retry_at = ?,\n processing_completed_at = ?,\n updated_at = ?,\n processor_id = NULL,\n lock_expires_at = NULL\n WHERE id = ?\n ","params":["PENDING",1,"Attempt 1 failed","{\"message\":\"Attempt 1 failed\",\"stack\":\"Error: Attempt 1 failed\\n at Object. (C:\\\\Users\\\\NEW OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.test.ts:254:15)\",\"timestamp\":\"2026-10-16T13:16:42.610Z\"}",null,null,"2026-10-16T13:16:42.610Z",43],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:46.253Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:46.253Z info: Database migrations completed +2026-10-16T13:16:46.254Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.267Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.300Z info: Notification marked as completed {"id":1} +2026-10-16T13:16:46.318Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.337Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.358Z info: Fetched and locked pending notifications {"count":1,"processorId":"test-processor"} +2026-10-16T13:16:46.369Z info: Database connection closed +2026-10-16T13:16:46.491Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.519Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:46.519Z info: Database migrations completed +2026-10-16T13:16:46.519Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.533Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.562Z info: Notification marked as completed {"id":4} +2026-10-16T13:16:46.575Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.590Z info: Notification cancelled {"id":5} +2026-10-16T13:16:46.597Z info: Database connection closed +2026-10-16T13:16:46.708Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.738Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:46.739Z info: Database migrations completed +2026-10-16T13:16:46.739Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.763Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.778Z info: Notification marked as completed {"id":6} +2026-10-16T13:16:46.781Z info: Database connection closed +2026-10-16T13:16:46.900Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.919Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:46.919Z info: Database migrations completed +2026-10-16T13:16:46.920Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:46.933Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.946Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T13:16:46.995Z info: Notification marked as completed {"id":7} +2026-10-16T13:16:47.035Z info: Notification marked as completed {"id":8} +2026-10-16T13:16:47.038Z info: Database connection closed +2026-10-16T13:16:47.149Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:47.166Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:47.167Z info: Database migrations completed +2026-10-16T13:16:47.167Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:47.183Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T13:16:47.237Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":9} +2026-10-16T13:16:47.281Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":9} +2026-10-16T13:16:47.315Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":3,"maxRetries":9} +2026-10-16T13:16:47.348Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":4,"maxRetries":9} +2026-10-16T13:16:47.386Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":5,"maxRetries":9} +2026-10-16T13:16:47.415Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":6,"maxRetries":9} +2026-10-16T13:16:47.445Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":7,"maxRetries":9} +2026-10-16T13:16:47.474Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":8,"maxRetries":9} +2026-10-16T13:16:47.529Z warn: Notification moved to dead letter queue {"id":9,"notificationType":"discord","failureReason":"Attempt 9 failed"} +2026-10-16T13:16:47.530Z info: Notification marked for retry or failed {"id":9,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} +2026-10-16T13:16:47.557Z info: Notification marked as completed {"id":9} +2026-10-16T13:16:47.567Z info: Database connection closed +2026-10-16T13:16:47.680Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:47.701Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:47.701Z info: Database migrations completed +2026-10-16T13:16:47.702Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T13:16:47.724Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T13:16:47.757Z info: Notification marked as completed {"id":10} +2026-10-16T13:16:47.774Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T13:16:47.815Z info: Notification marked as completed {"id":11} +2026-10-16T13:16:47.835Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T13:16:47.862Z info: Notification marked as completed {"id":12} +2026-10-16T13:16:47.882Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T13:16:47.911Z info: Notification marked as completed {"id":13} +2026-10-16T13:16:47.924Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T13:16:47.957Z info: Notification marked as completed {"id":14} +2026-10-16T13:16:47.973Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T13:16:48.001Z info: Notification marked for retry or failed {"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:48.025Z info: Notification marked as completed {"id":15} +2026-10-16T13:16:48.044Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T13:16:48.071Z info: Notification marked for retry or failed {"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:48.098Z info: Notification marked as completed {"id":16} +2026-10-16T13:16:48.112Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T13:16:48.145Z info: Notification marked for retry or failed {"id":17,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:48.172Z info: Notification marked as completed {"id":17} +2026-10-16T13:16:48.189Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T13:16:48.216Z info: Notification marked for retry or failed {"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:48.245Z info: Notification marked for retry or failed {"id":18,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:48.269Z info: Notification marked as completed {"id":18} +2026-10-16T13:16:48.282Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T13:16:48.313Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:48.341Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:48.372Z info: Notification marked as completed {"id":19} +2026-10-16T13:16:48.387Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} +2026-10-16T13:16:48.412Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:16:48.440Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:16:48.480Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T13:16:48.480Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:48.521Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T13:16:48.522Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:16:48.525Z info: Database connection closed +FAIL src/services/retry-deduplication.test.ts (15.548 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +2026-10-16T13:16:49.115Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:49.922Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:49.922Z info: Database migrations completed +2026-10-16T13:16:49.923Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:49.941Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:49.944Z info: Database connection closed +2026-10-16T13:16:49.948Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:49.966Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:49.967Z info: Database migrations completed +2026-10-16T13:16:49.967Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:49.985Z info: Fetched and locked pending notifications {"count":1,"processorId":"restarted-worker"} +2026-10-16T13:16:49.989Z info: Database connection closed +2026-10-16T13:16:49.999Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:50.806Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:50.806Z info: Database migrations completed +2026-10-16T13:16:50.807Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:50.828Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:50.846Z info: Fetched and locked pending notifications {"count":1,"processorId":"doomed-worker"} +2026-10-16T13:16:50.861Z info: Database connection closed +2026-10-16T13:16:50.865Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:50.881Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:50.882Z info: Database migrations completed +2026-10-16T13:16:50.882Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:50.906Z warn: Recovered stale locks {"count":1} +2026-10-16T13:16:50.924Z info: Fetched and locked pending notifications {"count":1,"processorId":"next-worker"} +2026-10-16T13:16:50.930Z info: Database connection closed +2026-10-16T13:16:50.937Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:51.716Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:51.717Z info: Database migrations completed +2026-10-16T13:16:51.717Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:51.732Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:51.763Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:51.779Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} +2026-10-16T13:16:51.780Z info: Database connection closed +2026-10-16T13:16:51.783Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:51.803Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:51.803Z info: Database migrations completed +2026-10-16T13:16:51.804Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:51.806Z info: Database connection closed +2026-10-16T13:16:51.816Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:52.647Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:52.648Z info: Database migrations completed +2026-10-16T13:16:52.648Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:52.664Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:52.681Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:16:52.697Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:16:52.714Z info: Fetched and locked pending notifications {"count":3,"processorId":"worker-1"} +2026-10-16T13:16:52.717Z info: Database connection closed +2026-10-16T13:16:52.724Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +FAIL src/__tests__/stress.test.ts (205.296 s) + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + +2026-10-16T13:16:53.506Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:53.506Z info: Database migrations completed +2026-10-16T13:16:53.507Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:53.530Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:53.545Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:16:53.561Z info: Fetched and locked pending notifications {"count":2,"processorId":"worker-1"} +2026-10-16T13:16:53.684Z warn: Recovered stale locks {"count":1} +2026-10-16T13:16:53.688Z info: Database connection closed +2026-10-16T13:16:53.697Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:54.672Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:54.673Z info: Database migrations completed +2026-10-16T13:16:54.673Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:54.703Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:54.729Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T13:16:54.786Z info: Notification marked as completed {"id":1} +2026-10-16T13:16:54.788Z info: Database connection closed +2026-10-16T13:16:54.793Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +PASS src/services/notification-metrics-store.test.ts (5.015 s) +2026-10-16T13:16:58.275Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:58.278Z info: Database migrations completed +2026-10-16T13:16:58.278Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:58.295Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:58.313Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T13:16:58.336Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T13:17:58.314Z"} +2026-10-16T13:16:58.368Z info: Fetched due retries {"count":1,"processorId":"retry-worker"} +2026-10-16T13:16:58.371Z info: Database connection closed +2026-10-16T13:16:58.380Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +FAIL src/services/scheduled-notification-queue.test.ts (10.513 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +2026-10-16T13:16:59.114Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:16:59.115Z info: Database migrations completed +2026-10-16T13:16:59.115Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T13:16:59.134Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:16:59.154Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T13:16:59.184Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"permanent outage"} +2026-10-16T13:16:59.185Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T13:16:59.189Z info: Database connection closed +2026-10-16T13:16:59.569Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:00.593Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:00.594Z info: Database migrations completed +2026-10-16T13:17:00.595Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:00.648Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:00.702Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:00.728Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:17:00.761Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:01.002Z info: Database connection closed +2026-10-16T13:17:01.013Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:01.856Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:01.857Z info: Database migrations completed +2026-10-16T13:17:01.857Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:01.885Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:01.918Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:01.938Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:01.984Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:02.031Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:02.053Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:02.090Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T13:17:02.155Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 2"} +2026-10-16T13:17:02.157Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T13:17:02.203Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 3"} +2026-10-16T13:17:02.205Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T13:17:02.239Z info: Database connection closed +2026-10-16T13:17:02.247Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:03.122Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:03.122Z info: Database migrations completed +2026-10-16T13:17:03.123Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:03.158Z info: Database connection closed +2026-10-16T13:17:03.165Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:04.349Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:04.350Z info: Database migrations completed +2026-10-16T13:17:04.350Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:04.375Z info: Database connection closed +2026-10-16T13:17:04.390Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:05.216Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:05.217Z info: Database migrations completed +2026-10-16T13:17:05.217Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T13:17:05.255Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:05.290Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +FAIL src/api/execution-metrics-api.test.ts (6.186 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +2026-10-16T13:17:05.342Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:05.384Z info: Database connection closed +2026-10-16T13:17:05.770Z info: Connected to SQLite database {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792156625768.db"} +2026-10-16T13:17:08.208Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:08.209Z info: Database migrations completed +2026-10-16T13:17:08.209Z info: Database initialized successfully {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792156625768.db"} +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:17:11.987Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:17:12.052Z info: Worker manager force shutdown completed +2026-06-24T12:05:01.400Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:05:01.400Z info: No active jobs - proceeding with shutdown +2026-06-24T12:05:01.400Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:17:12.130Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:17:12.220Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:17:12.317Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T13:17:12.397Z info: Worker manager force shutdown completed +FAIL src/database/query-performance.test.ts (8.415 s) + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   44 | }); +  45 | + > 46 | it('uses an indexed plan for pending claim queries', +async () => { +  | ^ +  47 | // Seed enough rows that a full scan would be meaningful +  48 | for (let i = 0; i < +200; i++) { +  49 | await db.run( + + at src/database/query-performance.test.ts:46:3 + at Object. (src/database/query-performance.test.ts:20:1) + +2026-10-16T13:17:13.826Z info: Database connection closed +2026-10-16T13:17:13.830Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n (payload, notification_type, target_recipient, execute_at, status, priority)\n VALUES (?, 'discord', ?, datetime('now', ?), 'PENDING', ?)","params":["{\"i\":184}","user-184","-184 seconds",5],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T13:17:13.831Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792156625768.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T13:17:14.270Z info: Worker manager force shutdown completed +2026-10-16T13:17:14.348Z info: Connected to SQLite database {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792156625768.db"} +2026-10-16T13:17:14.365Z warn: Database connection recovered {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792156625768.db"} +2026-10-16T13:17:14.370Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n (payload, notification_type, target_recipient, execute_at, status, priority)\n VALUES (?, 'discord', ?, datetime('now', ?), 'PENDING', ?)","params":["{\"i\":184}","user-184","-184 seconds",5],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:14.375Z info: Worker manager force shutdown completed +2026-10-16T13:17:14.491Z info: Worker manager force shutdown completed +FAIL src/api/events-server.test.ts + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality +2026-10-16T13:17:15.574Z info: Worker manager force shutdown completed + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +2026-10-16T13:17:15.783Z info: Worker manager force shutdown completed +2026-10-16T13:17:15.906Z info: Worker manager force shutdown completed +2026-10-16T13:17:16.020Z info: Worker manager force shutdown completed +2026-10-16T13:17:16.189Z info: Worker manager force shutdown completed +2026-10-16T13:17:16.319Z info: Worker manager force shutdown completed +FAIL src/__tests__/notification-flow-e2e.test.ts (18.577 s) + ● 1. Notification creation › rejects a duplicate idempotency key used with a different payload + + expect(received).rejects.toThrow(expected) + + Expected substring: "Idempotency key reused with different request body" + Received message: "Idempotency key reused with a different request body" + +   131 | if (!isValidRequest) { +  132 | logger.warn('Idempotency key reused with different request body +(replay suspected)', logMeta); + > 133 | throw new +IdempotencyKeyReuseError(); +  | ^ +  134 | } +  135 | +  136 | logger.info('Replay intercepted: returning cached idempotent +response', { + + at IdempotencyKeyService.processWithIdempotency (src/services/idempotency-key-service.ts:133:15) + at NotificationAPI.scheduleNotification (src/services/notification-api.ts:168:9) + at Object. (src/__tests__/notification-flow-e2e.test.ts:253:5) + at Object.toThrow (node_modules/expect/build/index.js:2155:20) + at Object. (src/__tests__/notification-flow-e2e.test.ts:259:15) + + ● 2. Delivery workflows › processes a due notification and transitions it to COMPLETED + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   343 | +  344 | const row = await repository.getById(id); + > 345 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  346 | expect(sendMock).toHaveBeenCalledTimes(1); +  347 | }); +  348 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:345:25) + + ● 2. Delivery workflows › delivers once executeAt elapses mid-run + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   377 | await jest.advanceTimersByTimeAsync(400); +  378 | + > 379 | expect((await repository.getById(id))!. +status).toBe(NotificationStatus.COMPLETED); +  | ^ +  380 | expect(sendMock).toHaveBeenCalledTimes(1); +  381 | }); +  382 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:379:52) + + ● 2. Delivery workflows › writes a SUCCESS execution log entry after delivery + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 0 + Received array: [] + +   396 | [id] +  397 | ); + > 398 | expect(logs).toHaveLength(1); +  | ^ +  399 | expect(logs[0].status).toBe('SUCCESS'); +  400 | expect(logs[0].execution_attempt).toBe(1); +  401 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:398:18) + + ● 2. Delivery workflows › processes a batch of multiple due notifications + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   418 | +  419 | for (const id of ids) { + > 420 | expect((await repository.getById(id))![33 +m.status).toBe(NotificationStatus.COMPLETED); +  | ^ +  421 | } +  422 | expect(sendMock).toHaveBeenCalledTimes(3); +  423 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:420:54) + + ● 2. Delivery workflows › forwards the event and contract config to the Discord service + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: ObjectContaining {"id": "deliver-evt-1"}, ObjectContaining {"address": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"}, Any + + Number of calls: 0 + +   438 | await jest.advanceTimersByTimeAsync(250); +  439 | + > 440 | expect(sendMock).toHaveBeenCalledWith( +  | ^ +  441 | expect.objectContaining({ id: 'deliver-evt-1' }), +  442 | expect.objectContaining({ address: contractConfig.address +}), +  443 | expect.any(String) + + at Object. (src/__tests__/notification-flow-e2e.test.ts:440:22) + + ● 3. Retry scenarios › prevents concurrent duplicate processing via distributed lock + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   587 | await p1; +  588 | + > 589 | expect(sendMock).toHaveBeenCalledTimes(1); +  | ^ +  590 | const row = await db.get<{ +status: string }>( +  591 | 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] +  592 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:589:22) + + ● 3. Retry scenarios › retries a failed delivery on the next scheduler poll cycle + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   620 | +  621 | const row = await repository.getById(id); + > 622 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  623 | expect(sendMock).toHaveBeenCalledTimes(2); +  624 | +  625 | await scheduler.stop(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:622:25) + + ● 4. Failure handling › marks a notification FAILED after exhausting all retries + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PROCESSING" + +   664 | 'SELECT status, retry_count FROM scheduled_notifications WHERE id = +?', [id] +  665 | ); + > 666 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  667 | }); +  668 | +  669 | it('logs a permanent-failure error after max retries are exhausted', +async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:666:25) + + ● 4. Failure handling › logs a permanent-failure error after max retries are exhausted + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"id": 2, "totalAttempts": 3} + Received: "Notification permanently failed after max retries", {"failureType": "unknown", "id": 2, "requestId": +"e2e-req-id", "totalAttempts": 4} + + Number of calls: 1 + +   677 | await retryScheduler.runOnce(); +  678 | + > 679 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  680 | 'Notification permanently failed after max retries', +  681 | expect.objectContaining({ id, totalAttempts: 3 }) +  682 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:679:26) + + ● 4. Failure handling › transitions scheduler-dispatched notification to FAILED when retries run out + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PENDING" + +   738 | +  739 | const row = await repository.getById(id); + > 740 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  741 | +  742 | await scheduler.stop(); +  743 | jest.useRealTimers(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:740:25) + + ● 4. Failure handling › does not recover a PROCESSING lock that has not yet expired + + SQLITE_ERROR: cannot start a transaction within a transaction + +2026-10-16T13:17:17.247Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:17.983Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:17.984Z info: Database migrations completed +2026-10-16T13:17:17.984Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:17.999Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:18.045Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:18.076Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T13:17:18.106Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:18.114Z info: Database connection closed +2026-10-16T13:17:18.232Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:19.016Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:19.017Z info: Database migrations completed +2026-10-16T13:17:19.018Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:19.035Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:19.068Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:19.082Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:19.109Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:19.142Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:19.164Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:19.202Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T13:17:19.250Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 2"} +2026-10-16T13:17:19.251Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T13:17:19.298Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 3"} +2026-10-16T13:17:19.298Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T13:17:19.304Z info: Database connection closed +2026-10-16T13:17:19.418Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:23.319Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:23.320Z info: Database migrations completed +2026-10-16T13:17:23.320Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:23.336Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:23.363Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:23.379Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:23.405Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:23.417Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:23.441Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:23.468Z info: Notification marked as completed {"id":3} +2026-10-16T13:17:23.485Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:17:23.513Z info: Notification marked for retry or failed {"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:23.542Z info: Notification marked as completed {"id":4} +2026-10-16T13:17:23.555Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T13:17:23.585Z info: Notification marked for retry or failed {"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:23.608Z info: Notification marked as completed {"id":5} +2026-10-16T13:17:23.624Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T13:17:23.651Z info: Notification marked for retry or failed {"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T13:17:23.689Z warn: Notification moved to dead letter queue {"id":6,"notificationType":"discord","failureReason":"Error 2"} +2026-10-16T13:17:23.689Z info: Notification marked for retry or failed {"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T13:17:23.732Z warn: Notification moved to dead letter queue {"id":6,"notificationType":"discord","failureReason":"Error 3"} +2026-10-16T13:17:23.732Z info: Notification marked for retry or failed {"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T13:17:23.742Z info: Database connection closed +2026-10-16T13:17:23.865Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:24.650Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:24.651Z info: Database migrations completed +2026-10-16T13:17:24.651Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:24.665Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:24.698Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:24.717Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:24.753Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:24.767Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:24.808Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error"} +2026-10-16T13:17:24.809Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} +2026-10-16T13:17:24.817Z info: Database connection closed +2026-10-16T13:17:24.932Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:25.757Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:25.758Z info: Database migrations completed +2026-10-16T13:17:25.758Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:25.766Z info: Database connection closed +2026-10-16T13:17:25.884Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:26.634Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:26.634Z info: Database migrations completed +2026-10-16T13:17:26.635Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T13:17:26.650Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:26.663Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:26.689Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:26.692Z info: Database connection closed +FAIL src/services/execution-metrics.test.ts (9.89 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +2026-10-16T13:17:27.114Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (12.255 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +2026-10-16T13:17:27.826Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:27.827Z info: Database migrations completed +2026-10-16T13:17:27.828Z info: Database initialized successfully {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T13:17:27.843Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:27.876Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"permanent failure"} +2026-10-16T13:17:27.877Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T13:17:32.844Z"} +2026-10-16T13:17:27.885Z info: Database connection closed +2026-10-16T13:17:27.890Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T13:17:28.084Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} +PASS src/services/dead-letter-queue.test.ts +2026-10-16T13:17:29.980Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:29.982Z info: Database migrations completed +2026-10-16T13:17:29.983Z info: Database initialized successfully {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T13:17:30.009Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} +2026-10-16T13:17:30.049Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"email","failureReason":"permanent failure"} +2026-10-16T13:17:30.050Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T13:17:35.010Z"} +2026-10-16T13:17:30.069Z info: Dead-letter notification requeued {"requestId":"req-2","id":1,"notificationId":1} +2026-10-16T13:17:30.072Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:30.073Z info: Database migrations completed +2026-10-16T13:17:30.073Z info: Database initialized successfully {"path":"./data/test-integrity.db"} +2026-10-16T13:17:30.075Z info: Database connection closed +2026-10-16T13:17:30.079Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T13:17:30.092Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:30.114Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T13:17:30.125Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:30.159Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T13:17:30.172Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:30.197Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +FAIL src/tests/payload-integrity.test.ts + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +2026-10-16T13:17:30.213Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:17:30.216Z info: Database connection closed +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +3761.04267570452, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T13:17:34.975Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +2026-10-16T13:17:31.608Z info: Connected to SQLite database {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-30084-1792156651605.db"} +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +2026-10-16T13:17:32.365Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:32.366Z info: Database migrations completed +2026-10-16T13:17:32.366Z info: Database initialized successfully {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-30084-1792156651605.db"} +2026-10-16T13:17:32.374Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILED","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T13:17:32.388Z info: Database connection closed +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +2026-10-16T13:17:34.554Z error: Failed to create template {"error":{"message":"uniqueKey: is required and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: uniqueKey: is required and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:85:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.562Z error: Failed to create template {"error":{"message":"channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:107:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.567Z error: Failed to create template {"error":{"message":"Invalid JSON body","name":"Error","stack":"Error: Invalid JSON body\n at EventEmitter. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:42:16)\n at EventEmitter.emit (node:events:509:28)\n at C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:26:9\n at processTicksAndRejections (node:internal/process/task_queues:85:11)"},"requestId":"req-1"} +2026-10-16T13:17:34.572Z error: Failed to create template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:141:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:137:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.580Z error: Failed to create template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:150:5)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:153:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.595Z error: Failed to list templates {"error":{"message":"channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} +2026-10-16T13:17:34.602Z info: Listed templates via API {"requestId":"req-1","count":0,"channelType":"EMAIL","activeOnly":false} +2026-10-16T13:17:34.616Z error: Failed to update template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:201:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.624Z error: Failed to update template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:215:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.630Z error: Failed to update template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:288:5)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:226:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.639Z error: Failed to render template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:237:7)"},"requestId":"req-1"} +FAIL src/services/discord-notification.test.ts + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +2026-10-16T13:17:34.648Z error: Failed to render template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:247:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.655Z error: Failed to render template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:360:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:261:7)"},"requestId":"req-1"} +2026-10-16T13:17:34.665Z error: Failed to get template stats {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} +2026-10-16T13:17:34.673Z error: Failed to get template stats {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} +2026-10-16T13:17:35.002Z info: Connected to SQLite database {"path":"./data/test-templates.db"} +FAIL src/api/template-routes.test.ts + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +2026-10-16T13:17:35.731Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:35.731Z info: Database migrations completed +2026-10-16T13:17:35.732Z info: Database initialized successfully {"path":"./data/test-templates.db"} +2026-10-16T13:17:35.737Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX IF NOT EXISTS idx_templates_unique_key \r\n ON notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:35.764Z info: Database connection closed +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4611.300108539392, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T13:17:40.399Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +2026-10-16T13:17:36.036Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +2026-10-16T13:17:37.156Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.157Z info: Database migrations completed +2026-10-16T13:17:37.158Z info: Database initialized successfully {"path":"./data/test-template-api.db"} +2026-10-16T13:17:37.160Z error: Database run error {"sql":"duplicate inserts with the same key are silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.164Z error: Database run error {"sql":"runMigrations tolerates duplicate-column errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.169Z error: Database run error {"sql":"runMigrations tolerates duplicate-column errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.170Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.172Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.173Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.174Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.175Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.177Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.178Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.197Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.200Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.202Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.204Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.206Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.207Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.209Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.211Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.214Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.216Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.218Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.220Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.221Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.223Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.224Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.225Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.230Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.232Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.234Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.235Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.236Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.238Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.239Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.241Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.244Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.247Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.250Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.251Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.253Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.254Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.255Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +FAIL src/tests/template-api-integration.test.ts + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +2026-10-16T13:17:37.257Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.259Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:37.263Z info: Database connection closed +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +2026-10-16T13:17:37.469Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.472Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.474Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.477Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.480Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.506Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.508Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.509Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T13:17:37.511Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792070257374} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792156658473, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792070257374} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792156658473, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "784a2fcf"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/tests/api-versioning.test.ts + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +2026-10-16T13:17:38.801Z info: Handling GET /api/events {"requestId":"62cd786d","correlationId":"43379f67-e736-4b3f-ac53-d902e09631c6","limit":20} +2026-10-16T13:17:38.802Z info: GET /api/events complete {"requestId":"62cd786d","correlationId":"43379f67-e736-4b3f-ac53-d902e09631c6","returned":0,"durationMs":3} +2026-10-16T13:17:38.807Z info: Handling GET /api/events {"requestId":"152f96ce","correlationId":"72e5909d-0c97-4a34-9db1-5646e310dda9","limit":20} +2026-10-16T13:17:38.808Z info: GET /api/events complete {"requestId":"152f96ce","correlationId":"72e5909d-0c97-4a34-9db1-5646e310dda9","returned":0,"durationMs":1} +2026-10-16T13:17:38.813Z info: Handling GET /api/events {"requestId":"29152ad1","correlationId":"5f5a3c91-1480-4e26-bd0c-1597e094e9af","limit":20} +2026-10-16T13:17:38.814Z info: GET /api/events complete {"requestId":"29152ad1","correlationId":"5f5a3c91-1480-4e26-bd0c-1597e094e9af","returned":0,"durationMs":1} +2026-10-16T13:17:38.816Z info: Handling GET /api/events {"requestId":"76c7a7f3","correlationId":"9b5eb6e8-354e-4b69-86eb-16a9897d6493","limit":20} +2026-10-16T13:17:38.816Z info: GET /api/events complete {"requestId":"76c7a7f3","correlationId":"9b5eb6e8-354e-4b69-86eb-16a9897d6493","returned":0,"durationMs":0} +2026-10-16T13:17:38.819Z info: Handling GET /api/events {"requestId":"9c9fc5bb","correlationId":"d62b682d-b8ca-4ef5-9ca0-984380614685","limit":20} +2026-10-16T13:17:38.820Z info: GET /api/events complete {"requestId":"9c9fc5bb","correlationId":"d62b682d-b8ca-4ef5-9ca0-984380614685","returned":0,"durationMs":1} +2026-10-16T13:17:38.831Z info: Handling GET /api/events {"requestId":"dcb73c68","correlationId":"1e5f3393-1d17-4a81-9682-d0fc73ac2d10","limit":20} +2026-10-16T13:17:38.832Z info: GET /api/events complete {"requestId":"dcb73c68","correlationId":"1e5f3393-1d17-4a81-9682-d0fc73ac2d10","returned":0,"durationMs":2} +2026-10-16T13:17:38.833Z info: Handling GET /api/events {"requestId":"29eaf56f","correlationId":"0028c736-1dad-468c-aba1-15f2cadad928","limit":20} +2026-10-16T13:17:38.833Z info: GET /api/events complete {"requestId":"29eaf56f","correlationId":"0028c736-1dad-468c-aba1-15f2cadad928","returned":0,"durationMs":1} +2026-10-16T13:17:38.837Z info: Handling GET /api/events {"requestId":"f7637e3b","correlationId":"6035f704-71e4-41a7-8083-093739dda86e","limit":20} +2026-10-16T13:17:38.837Z info: GET /api/events complete {"requestId":"f7637e3b","correlationId":"6035f704-71e4-41a7-8083-093739dda86e","returned":0,"durationMs":1} +2026-10-16T13:17:38.840Z info: Handling GET /api/events {"requestId":"0b4fca7f","correlationId":"3bc6c331-dc33-4dc1-9724-5fac382c7dfa","limit":5} +2026-10-16T13:17:38.841Z info: GET /api/events complete {"requestId":"0b4fca7f","correlationId":"3bc6c331-dc33-4dc1-9724-5fac382c7dfa","returned":0,"durationMs":1} +2026-10-16T13:17:38.846Z warn: Unhandled request {"requestId":"de1086dd","correlationId":"258b2b88-693e-46ed-a5f9-593c8aebc2a6","method":"GET","url":"/api/v1/unknown-route"} +2026-10-16T13:17:38.848Z info: Request completed {"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":2,"requestId":"de1086dd"} +2026-10-16T13:17:38.851Z warn: Unhandled request {"requestId":"e15b9ba2","correlationId":"bc153af1-435f-4885-8c37-03d7771b5bf7","method":"GET","url":"/api/v1/no-such-route"} +2026-10-16T13:17:38.852Z info: Request completed {"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":1,"requestId":"e15b9ba2"} +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "a9591165"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "f91a6f99-e1cd-4be0-895c-4a69878ec6b1", +"eventId": "event-1", "requestId": "f91a6f99-e1cd-4be0-895c-4a69878ec6b1"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +2026-10-16T13:17:39.178Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.180Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an integer, received 'abc'"} +2026-10-16T13:17:39.184Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.185Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= 100"} +2026-10-16T13:17:39.187Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.187Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= 0"} +2026-10-16T13:17:39.189Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.189Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} +2026-10-16T13:17:39.191Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.191Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be a valid date, received 'not-a-date'"} +2026-10-16T13:17:39.194Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.197Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T13:17:39.202Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} +2026-10-16T13:17:39.205Z info: Handling POST /api/archive/run {"requestId":"req-1"} +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +2026-10-16T13:17:39.507Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T13:17:39.508Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T13:17:39.508Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T13:17:39.510Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T13:17:39.510Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +2026-10-16T13:17:39.937Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T13:17:39.936Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} +2026-10-16T13:17:39.940Z info: ArchiveService: cycle complete {"archived":2,"purged":0,"durationMs":5,"jobId":"archive-cycle-1792156659934"} +2026-10-16T13:17:39.941Z info: Worker manager force shutdown completed +2026-10-16T13:17:39.945Z info: ArchiveService: archived processed notification immediately {"id":42} +2026-10-16T13:17:39.949Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":2,"jobId":"archive-cycle-1792156659947"} +2026-10-16T13:17:39.950Z info: Worker manager force shutdown completed +2026-10-16T13:17:39.952Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792156659951"} +2026-10-16T13:17:39.952Z info: Worker manager force shutdown completed +2026-10-16T13:17:39.954Z info: ArchiveService: purged expired archive records {"count":2,"cutoff":"2026-07-18T13:17:39.954Z"} +2026-10-16T13:17:39.955Z info: ArchiveService: cycle complete {"archived":0,"purged":2,"durationMs":1,"jobId":"archive-cycle-1792156659953"} +2026-10-16T13:17:39.955Z info: Worker manager force shutdown completed +2026-10-16T13:17:39.957Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792156659956"} +2026-10-16T13:17:39.958Z info: Worker manager force shutdown completed +2026-10-16T13:17:39.959Z info: ArchiveService started {"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} +2026-10-16T13:17:39.959Z info: Waiting for active archival jobs to complete {"activeJobs":1} +2026-10-16T13:17:39.959Z info: Initiating graceful shutdown {"activeJobs":1} +2026-10-16T13:17:39.965Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":6,"jobId":"archive-cycle-1792156659959"} +2026-10-16T13:17:39.966Z info: Worker manager force shutdown completed +2026-10-16T13:17:39.968Z info: Handling GET /api/archive {"requestId":"req-2"} +2026-10-16T13:17:40.101Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} +2026-10-16T13:17:40.102Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} +2026-10-16T13:17:40.109Z info: Handling POST /api/archive/run {"requestId":"req-6"} +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 1, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +PASS src/services/idempotency-key-service.test.ts +2026-10-16T13:17:42.256Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T13:17:42.275Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.280Z info: Database migrations completed +2026-10-16T13:17:42.281Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.287Z info: Database connection closed +2026-10-16T13:17:42.290Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T13:17:42.304Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.305Z info: Database migrations completed +2026-10-16T13:17:42.305Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.313Z info: Database connection closed +2026-10-16T13:17:42.318Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T13:17:42.334Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.334Z info: Database migrations completed +2026-10-16T13:17:42.335Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.340Z info: Event processed and recorded {"eventId":"duplicate-test-event","contractAddress":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a026ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} +2026-10-16T13:17:42.345Z info: Database connection closed +2026-10-16T13:17:42.348Z info: Connected to SQLite database {"path":":memory:"} +PASS src/scripts/replay-event.test.ts +2026-10-16T13:17:42.364Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.364Z info: Database migrations completed +2026-10-16T13:17:42.365Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.370Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAddress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} +2026-10-16T13:17:42.372Z info: Database connection closed +2026-10-16T13:17:42.375Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T13:17:42.398Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.399Z info: Database migrations completed +2026-10-16T13:17:42.399Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.402Z info: Database connection closed +2026-10-16T13:17:42.404Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T13:17:42.419Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.419Z info: Database migrations completed +2026-10-16T13:17:42.419Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.421Z info: Database connection closed +2026-10-16T13:17:42.422Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T13:17:42.436Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:42.436Z info: Database migrations completed +2026-10-16T13:17:42.436Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T13:17:42.439Z info: Database connection closed +2026-10-16T13:17:42.506Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +PASS src/config-schema.test.ts +2026-10-16T13:17:43.323Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:43.324Z info: Database migrations completed +2026-10-16T13:17:43.325Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:43.349Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:43.368Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:43.386Z info: Notification marked as completed {"id":2} +2026-10-16T13:17:43.416Z info: Database connection closed +2026-10-16T13:17:43.423Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:44.219Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:44.219Z info: Database migrations completed +2026-10-16T13:17:44.220Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:44.238Z info: Database connection closed +2026-10-16T13:17:44.253Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:44.865Z info: Handling GET /api/events {"requestId":"460ad25e","correlationId":"c02000f3-862b-4c80-975e-9e14b81245c1","limit":20} +2026-10-16T13:17:44.868Z info: GET /api/events complete {"requestId":"460ad25e","correlationId":"c02000f3-862b-4c80-975e-9e14b81245c1","returned":0,"durationMs":3} +2026-10-16T13:17:44.881Z info: Handling GET /api/events {"requestId":"94f2b1ba","correlationId":"my-trace-abc123","limit":20} +2026-10-16T13:17:44.882Z info: GET /api/events complete {"requestId":"94f2b1ba","correlationId":"my-trace-abc123","returned":0,"durationMs":1} +2026-10-16T13:17:44.887Z info: Handling GET /api/events {"requestId":"7c62cc2e","correlationId":"3763b1ab-d2a6-4487-b8d7-36505150b18b","limit":20} +2026-10-16T13:17:44.887Z info: GET /api/events complete {"requestId":"7c62cc2e","correlationId":"3763b1ab-d2a6-4487-b8d7-36505150b18b","returned":0,"durationMs":1} +2026-10-16T13:17:44.898Z warn: Unhandled request {"requestId":"2d829f23","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} +2026-10-16T13:17:44.900Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":4,"requestId":"2d829f23"} +2026-10-16T13:17:44.920Z warn: Unhandled request {"requestId":"4df6a0cb","correlationId":"8700bafa-8be9-4089-9218-b5d4ff65e52c","method":"GET","url":"/no-such-route"} +2026-10-16T13:17:44.921Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":1,"requestId":"4df6a0cb"} +2026-10-16T13:17:44.933Z warn: Unhandled request {"requestId":"bec2fe18","correlationId":"d8d81221-b98c-47cd-a8fe-9392c81a2983","method":"GET","url":"/no-such-route"} +2026-10-16T13:17:44.934Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":1,"requestId":"bec2fe18"} +2026-10-16T13:17:44.950Z info: Handling GET /api/events {"requestId":"2d3741b1","correlationId":"13ab9db5-30f8-45eb-a177-2da83817f3b6","limit":20} +2026-10-16T13:17:44.952Z info: GET /api/events complete {"requestId":"2d3741b1","correlationId":"13ab9db5-30f8-45eb-a177-2da83817f3b6","returned":20,"durationMs":1} +2026-10-16T13:17:44.957Z info: Handling GET /api/events {"requestId":"47fa5355","correlationId":"14b6d63e-f1af-45a1-a9eb-5d658ba08630","limit":50} +2026-10-16T13:17:44.958Z info: GET /api/events complete {"requestId":"47fa5355","correlationId":"14b6d63e-f1af-45a1-a9eb-5d658ba08630","returned":50,"durationMs":2} +PASS src/tests/events-server.test.ts +2026-10-16T13:17:44.968Z info: Handling GET /api/events {"requestId":"d2bf2bdd","correlationId":"77e22c15-7cb2-4430-96a3-cf097f8bf2b5","limit":100} +2026-10-16T13:17:44.969Z info: GET /api/events complete {"requestId":"d2bf2bdd","correlationId":"77e22c15-7cb2-4430-96a3-cf097f8bf2b5","returned":100,"durationMs":2} +2026-10-16T13:17:44.979Z info: Handling GET /api/events {"requestId":"e0189033","correlationId":"bd43ca01-3dfd-450a-8dec-9350acfe39f0","limit":1} +2026-10-16T13:17:44.980Z info: GET /api/events complete {"requestId":"e0189033","correlationId":"bd43ca01-3dfd-450a-8dec-9350acfe39f0","returned":1,"durationMs":2} +2026-10-16T13:17:44.987Z info: Handling GET /api/events {"requestId":"7953ce17","correlationId":"bb41e164-58c1-458b-9c42-cde9ffa10bfb","limit":20} +2026-10-16T13:17:44.988Z info: GET /api/events complete {"requestId":"7953ce17","correlationId":"bb41e164-58c1-458b-9c42-cde9ffa10bfb","returned":20,"durationMs":1} +2026-10-16T13:17:45.058Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:45.060Z info: Database migrations completed +2026-10-16T13:17:45.067Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:45.086Z info: Handling GET /api/schedule/queue {"requestId":"230f8c24","correlationId":"11e469dc-85be-4cb2-be84-bc216ea82819","count":0,"durationMs":12} +2026-10-16T13:17:45.091Z info: Database connection closed +2026-10-16T13:17:45.099Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:45.930Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:45.930Z info: Database migrations completed +2026-10-16T13:17:45.930Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:45.948Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:45.952Z info: Handling GET /api/schedule/queue {"requestId":"32413f9b","correlationId":"159c46ce-0a6c-4179-8ecc-aed82b0f1843","count":1,"durationMs":1} +2026-10-16T13:17:45.957Z info: Database connection closed +2026-10-16T13:17:45.963Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:46.259Z info: Connected to SQLite database {"path":"./data/test-notifications-refactored.db"} +2026-10-16T13:17:46.848Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:46.848Z info: Database migrations completed +2026-10-16T13:17:46.848Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:46.868Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:46.881Z info: Notification marked as completed {"id":1} +2026-10-16T13:17:46.899Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:46.939Z warn: Notification moved to dead letter queue {"id":2,"notificationType":"discord","failureReason":"oops"} +2026-10-16T13:17:46.940Z info: Notification marked for retry or failed {"id":2,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} +2026-10-16T13:17:46.954Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:46.958Z info: Handling GET /api/schedule/queue {"requestId":"1bc765ca","correlationId":"a42d49e3-1a9f-42f9-8ac5-bf3aa565d6e9","count":1,"durationMs":2} +2026-10-16T13:17:46.962Z info: Database connection closed +2026-10-16T13:17:46.966Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:47.298Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:47.300Z info: Database migrations completed +2026-10-16T13:17:47.300Z info: Database initialized successfully {"path":"./data/test-notifications-refactored.db"} +2026-10-16T13:17:47.336Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.374Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.392Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.413Z info: Fetched and locked pending notifications {"count":2,"processorId":"test-processor-1"} +2026-10-16T13:17:47.451Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.471Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T13:17:47.510Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.527Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T13:17:47.562Z warn: Recovered stale locks {"count":1} +2026-10-16T13:17:47.609Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.628Z info: Notification marked as completed {"id":6} +2026-10-16T13:17:47.665Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.687Z info: Notification marked for retry or failed {"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T13:17:47.727Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.760Z warn: Notification moved to dead letter queue {"id":8,"notificationType":"discord","failureReason":"Test error"} +2026-10-16T13:17:47.761Z info: Notification marked for retry or failed {"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T13:17:47.797Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.815Z info: Notification cancelled {"id":9} +2026-10-16T13:17:47.870Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.885Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.922Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T13:17:47.940Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T13:17:47.978Z warn: Recovered stale locks {"count":1} +2026-10-16T13:17:48.016Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:48.017Z info: Database migrations completed +2026-10-16T13:17:48.017Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:48.030Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.037Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.046Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T13:17:48.051Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.070Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.079Z warn: Recovered stale locks {"count":1} +2026-10-16T13:17:48.084Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.101Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.105Z info: Handling GET /api/schedule/queue {"requestId":"ca9695f2","correlationId":"0199673c-fa4c-4e85-a47d-6447341a10b4","count":3,"durationMs":2} +2026-10-16T13:17:48.109Z info: Database connection closed +2026-10-16T13:17:48.114Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:48.150Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.169Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.185Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T13:17:48.215Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.238Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +2026-10-16T13:17:48.256Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.283Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +2026-10-16T13:17:48.304Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.336Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +2026-10-16T13:17:48.353Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.355Z info: Scheduling new notification {"type":"email","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +2026-10-16T13:17:48.370Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"email"} +2026-10-16T13:17:48.371Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +2026-10-16T13:17:48.390Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"webhook"} +2026-10-16T13:17:48.392Z info: Scheduling new notification {"type":"sms","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +2026-10-16T13:17:48.406Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"sms"} +2026-10-16T13:17:48.428Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} +PASS src/tests/notification-scheduler-refactored.test.ts +2026-10-16T13:17:48.446Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} +2026-10-16T13:17:48.451Z info: Database connection closed +2026-10-16T13:17:48.660Z info: Connected to SQLite database {"path":"./data/test-scheduler-concurrency.db"} +2026-10-16T13:17:49.086Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:49.087Z info: Database migrations completed +2026-10-16T13:17:49.087Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:49.102Z info: Database connection closed +2026-10-16T13:17:49.106Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:49.679Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:49.680Z info: Database migrations completed +2026-10-16T13:17:49.680Z info: Database initialized successfully {"path":"./data/test-scheduler-concurrency.db"} +2026-10-16T13:17:49.684Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} +2026-10-16T13:17:50.089Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:50.090Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} +2026-10-16T13:17:50.499Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T13:17:50.500Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} +2026-10-16T13:17:50.599Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T13:17:50.599Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} +2026-10-16T13:17:50.622Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T13:17:50.858Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:50.858Z info: Database migrations completed +2026-10-16T13:17:50.858Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:50.875Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T13:17:50.890Z info: Handling GET /api/schedule/queue {"requestId":"9dab5b16","correlationId":"3d31de8f-fd30-4eb4-8ff3-be4df460125c","count":1,"durationMs":10} +2026-10-16T13:17:50.898Z info: Database connection closed +2026-10-16T13:17:50.929Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:51.664Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:51.664Z info: Database migrations completed +2026-10-16T13:17:51.665Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:51.675Z info: Database connection closed +2026-10-16T13:17:51.679Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:51.951Z info: Fetched and locked pending notifications {"requestId":"29ca842d","count":4,"processorId":"test-uuid-0001"} +2026-10-16T13:17:51.952Z info: Processing batch of scheduled notifications {"requestId":"29ca842d","count":4,"processorId":"test-uuid-0001"} +2026-10-16T13:17:51.954Z info: Processing scheduled notification {"requestId":"29ca842d","id":1,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:52.054Z info: Notification marked as completed {"requestId":"29ca842d","id":1} +2026-10-16T13:17:52.076Z info: Notification delivered successfully {"requestId":"29ca842d","id":1,"type":"discord","durationMs":84} +2026-10-16T13:17:52.078Z info: Processing scheduled notification {"requestId":"29ca842d","id":2,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:52.183Z info: Notification marked as completed {"requestId":"29ca842d","id":2} +2026-10-16T13:17:52.200Z info: Notification delivered successfully {"requestId":"29ca842d","id":2,"type":"discord","durationMs":86} +2026-10-16T13:17:52.201Z info: Processing scheduled notification {"requestId":"29ca842d","id":3,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:52.309Z info: Notification marked as completed {"requestId":"29ca842d","id":3} +2026-10-16T13:17:52.326Z info: Notification delivered successfully {"requestId":"29ca842d","id":3,"type":"discord","durationMs":85} +2026-10-16T13:17:52.328Z info: Processing scheduled notification {"requestId":"29ca842d","id":4,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:52.437Z info: Notification marked as completed {"requestId":"29ca842d","id":4} +2026-10-16T13:17:52.454Z info: Notification delivered successfully {"requestId":"29ca842d","id":4,"type":"discord","durationMs":85} +2026-10-16T13:17:52.461Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:52.462Z info: Database migrations completed +2026-10-16T13:17:52.461Z info: Scheduler batch complete {"requestId":"29ca842d","processorId":"test-uuid-0001","count":4,"durationMs":527} +2026-10-16T13:17:52.463Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:52.474Z info: Worker manager force shutdown completed +2026-10-16T13:17:52.477Z info: NotificationHealthMonitor started {"intervalMs":60000} +2026-10-16T13:17:52.498Z info: NotificationHealthMonitor stopped +2026-10-16T13:17:52.517Z info: Database connection closed +2026-10-16T13:17:52.518Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T13:17:52.519Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T13:17:52.524Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:52.527Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} +2026-10-16T13:17:52.554Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T13:17:52.556Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} +2026-10-16T13:17:52.576Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T13:17:52.577Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} +2026-10-16T13:17:52.596Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T13:17:52.597Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} +2026-10-16T13:17:52.615Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T13:17:52.616Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} +2026-10-16T13:17:52.633Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T13:17:52.634Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} +2026-10-16T13:17:52.638Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:52.639Z warn: Database connection recovered {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:52.731Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T13:17:53.888Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:17:53.899Z info: Database migrations completed +2026-10-16T13:17:53.900Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T13:17:53.909Z info: NotificationHealthMonitor started {"intervalMs":60000} +2026-10-16T13:17:53.918Z info: NotificationHealthMonitor stopped +2026-10-16T13:17:53.938Z info: Database connection closed +PASS src/api/queue-visibility.test.ts (11.995 s) +2026-10-16T13:17:54.288Z info: Fetched and locked pending notifications {"requestId":"88a30463","count":6,"processorId":"test-uuid-0002"} +2026-10-16T13:17:54.290Z info: Processing batch of scheduled notifications {"requestId":"88a30463","count":6,"processorId":"test-uuid-0002"} +2026-10-16T13:17:54.294Z info: Processing scheduled notification {"requestId":"88a30463","id":5,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:54.295Z info: Processing scheduled notification {"requestId":"88a30463","id":6,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:54.297Z info: Processing scheduled notification {"requestId":"88a30463","id":7,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:54.431Z info: Notification marked as completed {"requestId":"88a30463","id":6} +2026-10-16T13:17:54.432Z info: Notification marked as completed {"requestId":"88a30463","id":5} +2026-10-16T13:17:54.433Z info: Notification marked as completed {"requestId":"88a30463","id":7} +2026-10-16T13:17:54.480Z info: Notification delivered successfully {"requestId":"88a30463","id":6,"type":"discord","durationMs":100} +2026-10-16T13:17:54.481Z info: Processing scheduled notification {"requestId":"88a30463","id":8,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:54.482Z info: Notification delivered successfully {"requestId":"88a30463","id":5,"type":"discord","durationMs":100} +2026-10-16T13:17:54.482Z info: Processing scheduled notification {"requestId":"88a30463","id":9,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:54.483Z info: Notification delivered successfully {"requestId":"88a30463","id":7,"type":"discord","durationMs":99} +2026-10-16T13:17:54.484Z info: Processing scheduled notification {"requestId":"88a30463","id":10,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T13:17:54.761Z info: Notification marked as completed {"requestId":"88a30463","id":8} +2026-10-16T13:17:54.763Z info: Notification marked as completed {"requestId":"88a30463","id":9} +2026-10-16T13:17:54.764Z info: Notification marked as completed {"requestId":"88a30463","id":10} +2026-10-16T13:17:54.798Z info: Notification delivered successfully {"requestId":"88a30463","id":8,"type":"discord","durationMs":122} +2026-10-16T13:17:54.799Z info: Notification delivered successfully {"requestId":"88a30463","id":9,"type":"discord","durationMs":121} +2026-10-16T13:17:54.800Z info: Notification delivered successfully {"requestId":"88a30463","id":10,"type":"discord","durationMs":119} +2026-10-16T13:17:54.800Z info: Scheduler batch complete {"requestId":"88a30463","processorId":"test-uuid-0002","count":6,"durationMs":597} +2026-10-16T13:17:54.802Z info: Worker manager force shutdown completed +2026-10-16T13:17:54.833Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} +2026-10-16T13:17:54.846Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T13:17:54.847Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} +2026-10-16T13:17:54.861Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +PASS src/services/stellar-rpc-manager.test.ts +2026-10-16T13:17:55.213Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +2026-10-16T13:19:45.300Z info: Database connection closed +2026-10-16T13:19:45.306Z error: Database all error {"sql":"\n SELECT * FROM scheduled_notifications\n WHERE status = ?\n AND lock_expires_at IS NOT NULL\n AND lock_expires_at < ?\n ","params":["PROCESSING","2026-10-16T13:19:43.869Z"],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T13:19:46.599Z error: Rollback failed after transaction error {"error":{"message":"Database not initialized","name":"Error","stack":"Error: Database not initialized\n at Database.executeWithRecovery (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\database.ts:85:42)\n at Database.run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\database.ts:270:17)\n at Database.transaction (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\database.ts:358:20)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at ScheduledNotificationRepository.recoverStaleLocks (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\scheduled-notification-repository.ts:270:5)\n at NotificationScheduler.processPendingNotifications (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\notification-scheduler.ts:128:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\__tests__\\scheduler-concurrency.test.ts:136:5)"}} +2026-10-16T13:19:46.634Z error: Transaction rolled back {"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T13:19:46.636Z error: Error in scheduler processing loop {"requestId":"3ed28634","error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}","processorId":"test-uuid-0003","durationMs":2767} +FAIL src/__tests__/scheduler-concurrency.test.ts (119.405 s) + ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   123 | }); +  124 | + > 125 | test('concurrency larger than the batch is clamped to the batch +size', async () => { +  | ^ +  126 | await seedDueNotifications(2); +  127 | await sleep(1300); +  128 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:125:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +2026-10-16T13:44:47.928Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T13:44:48.134Z info: Database migrations completed +2026-10-16T13:44:48.136Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} +FAIL src/__tests__/database-recovery.test.ts (1612.13 s) + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   29 | }); +  30 | + > 31 | test('recovers transparently when the handle is closed +mid-flight', async () => { +  | ^ +  32 | const db = new Database(testDbPath, { +maxReconnectAttempts: 3, reconnectBaseDelayMs: 10 }); +  33 | await db.initialize(); +  34 | await db.run('CREATE TABLE t (id INTEGER PRIMARY KEY, v +TEXT)'); + + at src/__tests__/database-recovery.test.ts:31:3 + at Object. (src/__tests__/database-recovery.test.ts:18:1) + + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + +PASS src/config/schema-validator.test.ts (5.164 s) +PASS src/schema/compatibility-check.test.ts +PASS src/utils/json-logger-formatter.test.ts +2026-10-16T14:18:16.932Z error: Database run error {"sql":"INSERT INTO t (v) VALUES ('after')","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T14:18:16.943Z warn: Database connection failure detected; attempting recovery {"path":"./data/test-db-recovery.db","attempt":1,"maxReconnectAttempts":3,"delayMs":10,"error":"SQLITE_MISUSE: Database is closed"} +2026-10-16T14:18:18.540Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T14:18:18.551Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T14:18:18.606Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:18.607Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:18.609Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T14:18:18.634Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:18.638Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:18.681Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:18.715Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160299683} +2026-10-16T14:18:18.752Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T14:18:18.775Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:18.789Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:18.820Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160299791} +2026-10-16T14:18:18.825Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T14:18:18.831Z warn: Circuit breaker is OPEN, blocking request {"state":"open","nextAttemptTime":1792160299791} +2026-10-16T14:18:18.920Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:18.923Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:18.924Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160299924} +2026-10-16T14:18:18.925Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T14:18:18.987Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +2026-10-16T14:18:18.995Z warn: Database connection recovered {"path":"./data/test-db-recovery.db"} +2026-10-16T14:18:19.078Z info: Database connection closed +2026-10-16T14:18:20.229Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt {"previousState":"open","newState":"half_open"} +2026-10-16T14:18:20.284Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:20.285Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:20.286Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160301285} +2026-10-16T14:18:20.314Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T14:18:21.516Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt {"previousState":"open","newState":"half_open"} +2026-10-16T14:18:21.686Z info: Circuit breaker recorded success {"state":"half_open","successCount":1,"failureCount":3} +2026-10-16T14:18:21.825Z info: Circuit breaker closed after successful recovery {"successCount":0,"successThreshold":2} +2026-10-16T14:18:21.833Z info: Circuit breaker recorded success {"state":"closed","successCount":0,"failureCount":0} +2026-10-16T14:18:21.968Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:21.979Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:21.980Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160302980} +2026-10-16T14:18:21.981Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T14:18:23.197Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt {"previousState":"open","newState":"half_open"} +PASS src/services/circuit-breaker.test.ts (6.157 s) +2026-10-16T14:18:23.197Z info: Circuit breaker recorded success {"state":"half_open","successCount":1,"failureCount":3} +2026-10-16T14:18:23.199Z error: Circuit breaker opened due to repeated failures {"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160304199} +2026-10-16T14:18:23.199Z warn: Circuit breaker recorded failure {"state":"open","failureCount":4,"successCount":0} +2026-10-16T14:18:23.203Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:23.204Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:23.204Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160304204} +2026-10-16T14:18:23.205Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T14:18:23.205Z info: Circuit breaker manually reset {"previousState":"closed"} +2026-10-16T14:18:23.210Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T14:18:23.211Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T14:18:23.211Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":3,"successCount":0} +2026-10-16T14:18:23.211Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":4,"successCount":0} +2026-10-16T14:18:23.212Z error: Circuit breaker opened due to repeated failures {"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792160304212} +2026-10-16T14:18:23.212Z warn: Circuit breaker recorded failure {"state":"open","failureCount":5,"successCount":0} +2026-10-16T14:18:23.214Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +PASS src/utils/app-version.test.ts (3523.747 s) +PASS src/api/templates-api.test.ts (9.663 s) +PASS src/services/subscription-service.test.ts +PASS src/api/content-type.test.ts +2026-10-16T14:18:34.668Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} +2026-10-16T14:18:35.534Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:35.536Z info: Database migrations completed +2026-10-16T14:18:35.536Z info: Database initialized successfully {"path":"./data/test-notifications.db"} +2026-10-16T14:18:35.588Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T14:18:35.617Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} +2026-10-16T14:18:35.621Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} +2026-10-16T14:18:35.627Z info: Processing scheduled notification {"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} +2026-10-16T14:18:35.628Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":1059} +2026-10-16T14:18:35.670Z info: Notification marked as completed {"requestId":"test-request-id","id":1} +2026-10-16T14:18:35.686Z info: Notification delivered successfully {"requestId":"test-request-id","id":1,"type":"webhook","durationMs":19} +2026-10-16T14:18:35.693Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"receipt-success-test","count":1,"durationMs":98} +2026-10-16T14:18:35.711Z info: Worker manager force shutdown completed +2026-10-16T14:18:35.791Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} +2026-10-16T14:18:35.809Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} +2026-10-16T14:18:35.810Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} +2026-10-16T14:18:35.812Z info: Processing scheduled notification {"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} +2026-10-16T14:18:35.813Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":1038} +2026-10-16T14:18:35.844Z error: Failed to process notification {"requestId":"test-request-id","id":2,"error":{"message":"secret token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":4} +2026-10-16T14:18:35.901Z error: Job monitor: job failed {"jobId":"notification-2","jobName":"scheduled-notification","error":"secret token=should-not-persist","durationMs":89} +2026-10-16T14:18:35.932Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T14:18:40.906Z"} +2026-10-16T14:18:35.967Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":175} +2026-10-16T14:18:35.983Z info: Worker manager force shutdown completed +2026-10-16T14:18:36.125Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.184Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.221Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} +2026-10-16T14:18:36.283Z info: Expired notification skipped before delivery {"requestId":"test-request-id","id":4} +2026-10-16T14:18:36.411Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.439Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} +2026-10-16T14:18:36.441Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} +2026-10-16T14:18:36.442Z info: Processing scheduled notification {"requestId":"test-request-id","id":5,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T14:18:36.442Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":5,"executeAt":{},"now":{},"missedByMs":60051} +2026-10-16T14:18:36.443Z error: Failed to process notification {"requestId":"test-request-id","id":5,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":1} +2026-10-16T14:18:36.444Z error: Job monitor: job failed {"jobId":"notification-5","jobName":"scheduled-notification","error":"temporary outage","durationMs":3} +2026-10-16T14:18:36.478Z info: Notification marked for retry or failed {"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T14:18:36.544Z"} +2026-10-16T14:18:36.492Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":79} +2026-10-16T14:18:36.550Z info: Fetched due retries {"requestId":"test-request-id","count":1,"processorId":"retry-expiration-test"} +2026-10-16T14:18:36.550Z info: RetryScheduler processing batch {"requestId":"test-request-id","processorId":"retry-expiration-test","count":1} +2026-10-16T14:18:36.573Z info: Expired retry skipped before delivery {"requestId":"test-request-id","id":5} +2026-10-16T14:18:36.576Z info: Worker manager force shutdown completed +2026-10-16T14:18:36.622Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.634Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.654Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} +2026-10-16T14:18:36.655Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} +2026-10-16T14:18:36.657Z info: Processing scheduled notification {"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T14:18:36.658Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1047} +2026-10-16T14:18:36.679Z info: Notification marked as completed {"requestId":"test-request-id","id":6} +2026-10-16T14:18:36.693Z info: Notification delivered successfully {"requestId":"test-request-id","id":6,"type":"discord","durationMs":1} +2026-10-16T14:18:36.694Z info: Processing scheduled notification {"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T14:18:36.694Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":1072} +2026-10-16T14:18:36.725Z info: Notification marked as completed {"requestId":"test-request-id","id":7} +2026-10-16T14:18:36.741Z info: Notification delivered successfully {"requestId":"test-request-id","id":7,"type":"discord","durationMs":0} +2026-10-16T14:18:36.742Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":107} +2026-10-16T14:18:36.746Z info: Worker manager force shutdown completed +2026-10-16T14:18:36.799Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.818Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.833Z info: Fetched and locked pending notifications {"count":2,"processorId":"test-processor-1"} +2026-10-16T14:18:36.873Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T14:18:36.893Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} +2026-10-16T14:18:36.894Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} +2026-10-16T14:18:36.895Z info: Processing scheduled notification {"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T14:18:36.896Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60038} +2026-10-16T14:18:36.897Z error: Failed to process notification {"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":2} +2026-10-16T14:18:36.898Z error: Job monitor: job failed {"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":3} +2026-10-16T14:18:36.916Z info: Notification marked for retry or failed {"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T14:18:38.899Z"} +2026-10-16T14:18:36.926Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":51} +2026-10-16T14:18:36.958Z info: Fetched due retries {"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} +2026-10-16T14:18:36.959Z info: RetryScheduler processing batch {"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} +2026-10-16T14:18:36.960Z info: Retrying notification {"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} +2026-10-16T14:18:36.973Z info: Notification marked as completed {"requestId":"test-request-id","id":10} +2026-10-16T14:18:36.983Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} +2026-10-16T14:18:36.986Z info: Worker manager force shutdown completed +2026-10-16T14:18:37.043Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.062Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T14:18:37.129Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +PASS src/utils/circuit-breaker.test.ts +2026-10-16T14:18:37.153Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T14:18:37.231Z warn: Recovered stale locks {"count":1} +2026-10-16T14:18:37.310Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.341Z info: Notification marked as completed {"id":13} +2026-10-16T14:18:37.428Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.465Z info: Notification marked for retry or failed {"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T14:18:37.516Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.558Z warn: Notification moved to dead letter queue {"id":15,"notificationType":"discord","failureReason":"Test error"} +2026-10-16T14:18:37.558Z info: Notification marked for retry or failed {"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T14:18:37.608Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.624Z info: Notification cancelled {"id":16} +2026-10-16T14:18:37.691Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.717Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} +2026-10-16T14:18:37.820Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.833Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T14:18:37.884Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} +2026-10-16T14:18:37.915Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} +PASS src/services/payload-validation.integration.test.ts +2026-10-16T14:18:38.019Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} +2026-10-16T14:18:38.034Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} +2026-10-16T14:18:38.076Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T14:18:38.097Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} +2026-10-16T14:18:38.142Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} +2026-10-16T14:18:38.163Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} +2026-10-16T14:18:38.176Z info: Database connection closed +2026-10-16T14:18:38.183Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} +PASS src/api/cors-validator.test.ts +2026-10-16T14:18:39.158Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:39.158Z info: Database migrations completed +2026-10-16T14:18:39.159Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} +2026-10-16T14:18:39.237Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T14:18:39.297Z info: Notification marked as completed {"id":1} +2026-10-16T14:18:39.418Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T14:18:39.501Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T14:18:39.560Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T14:18:39.608Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} +2026-10-16T14:18:39.609Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T14:18:39.775Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T14:18:39.863Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-stale"} +2026-10-16T14:18:39.902Z warn: Recovered stale locks {"count":1} +PASS src/tests/notification-scheduler.test.ts (5.921 s) +2026-10-16T14:18:40.031Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T14:18:40.053Z info: Notification marked as completed {"id":5} +2026-10-16T14:18:40.091Z info: Database connection closed +PASS src/services/indexing-reconciliation-engine.test.ts +2026-10-16T14:18:42.297Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:42.299Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:42.299Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:42.299Z info: Worker manager force shutdown completed +2026-10-16T14:18:42.595Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:42.595Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:42.596Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:42.596Z info: Worker manager force shutdown completed +2026-10-16T14:18:43.367Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:43.367Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:43.367Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:43.368Z info: Worker manager force shutdown completed +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts +2026-10-16T14:18:43.837Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:43.839Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:43.840Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:43.841Z info: Worker manager force shutdown completed +PASS src/utils/logger.test.ts +2026-10-16T14:18:44.515Z info: Template created successfully {"templateId":1,"uniqueKey":"welcome-email"} +2026-10-16T14:18:44.565Z error: Failed to create template {"error":{"message":"name: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T14:18:44.575Z error: Failed to create template {"error":{"message":"name: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" ","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T14:18:44.580Z error: Failed to create template {"error":{"message":"channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T14:18:44.589Z error: Failed to create template {"error":{"message":"description: must be a string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!","description":12345}} +2026-10-16T14:18:44.593Z error: Failed to create template {"error":{"message":"name: must not exceed 255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T14:18:44.600Z error: Failed to create template {"error":{"message":"variables: must be an array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!","variables":[1,2,3]}} +2026-10-16T14:18:44.609Z error: Failed to create template {"error":{"message":"defaultValues: must be an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!","defaultValues":"nope"}} +2026-10-16T14:18:44.616Z info: Template updated successfully {"id":1} +PASS src/services/template-service.test.ts +2026-10-16T14:18:44.622Z error: Failed to update template {"error":{"message":"name: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} +2026-10-16T14:18:44.626Z info: Template updated successfully {"id":1} +2026-10-16T14:18:44.632Z error: Failed to update template {"error":{"message":"defaultValues: must be an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} +PASS src/api/notifications-history.test.ts +PASS src/examples/webhook-consumer-verification-example.test.ts +PASS src/utils/request-id.test.ts +2026-10-16T14:18:45.724Z info: Migration 004 (database-data-integrity) applied successfully +2026-10-16T14:18:45.795Z info: Migration 004-repeat (database-data-integrity) applied successfully +2026-10-16T14:18:45.809Z info: Worker manager force shutdown completed +PASS src/migrations/004-database-data-integrity.test.ts (5.718 s) +2026-10-16T14:18:45.837Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} +2026-10-16T14:18:45.848Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:45.858Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:45.859Z info: Database migrations completed +2026-10-16T14:18:45.860Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:45.862Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', '2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T14:18:45.863Z info: Database connection closed +PASS src/utils/metadata-validator.test.ts +PASS src/services/delivery-receipt-repository.test.ts +PASS src/services/database-cleanup-job.test.ts +2026-10-16T14:18:46.312Z info: Worker manager force shutdown completed +2026-10-16T14:18:46.349Z info: Worker manager force shutdown completed +2026-10-16T14:18:46.392Z info: Worker manager force shutdown completed +PASS src/providers/provider-capabilities.test.ts +PASS src/services/backpressure-controller.test.ts +2026-10-16T14:18:46.596Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.605Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.607Z info: Backpressure deactivated: queue recovered {"queueSize":49,"threshold":50,"durationMs":2,"targetThroughputPerSec":100} +2026-10-16T14:18:46.609Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.612Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.613Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.617Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.622Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.623Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T14:18:46.885Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"dfe68c38","correlationId":"ed07e9c6-3cda-49a8-b40d-f9c716eddab8","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":2} +2026-10-16T14:18:46.910Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"e16eb94d","correlationId":"ba766c84-4f35-4fdb-962f-0fe2c2e175ef","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":1} +2026-10-16T14:18:46.919Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"b781fb20","correlationId":"01053cfe-78a9-4d2e-aebf-225c7c92ea7f","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs":0} +2026-10-16T14:18:46.922Z info: Delivery metrics window reset after read {"requestId":"b781fb20","correlationId":"01053cfe-78a9-4d2e-aebf-225c7c92ea7f"} +2026-10-16T14:18:46.931Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"3aaddb59","correlationId":"52b98354-44e6-4725-8d88-95b39c39b1de","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":0} +2026-10-16T14:18:46.942Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"33491275","correlationId":"92b9a7e1-de1f-409d-b7b5-910e61a06215","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":0} +2026-10-16T14:18:46.950Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"986b02a3","correlationId":"afe0426a-44ba-42f2-9f7c-d62552bfe6da","limit":50} +2026-10-16T14:18:46.951Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"986b02a3","correlationId":"afe0426a-44ba-42f2-9f7c-d62552bfe6da","count":1,"durationMs":2} +2026-10-16T14:18:46.967Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"b3230b6f","correlationId":"b3879ca9-2c4c-445f-ab09-53b635170b03","limit":10} +2026-10-16T14:18:46.968Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"b3230b6f","correlationId":"b3879ca9-2c4c-445f-ab09-53b635170b03","count":0,"durationMs":1} +2026-10-16T14:18:46.976Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"f3b8aee1","correlationId":"4af280f2-37c5-4820-af11-f70e1798198d","limit":50,"since":"2024-01-01T00:00:00.000Z"} +2026-10-16T14:18:46.977Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"f3b8aee1","correlationId":"4af280f2-37c5-4820-af11-f70e1798198d","count":0,"durationMs":1} +PASS src/api/delivery-metrics-api.test.ts +2026-10-16T14:18:47.000Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"218ba7a6","correlationId":"16e06a83-70f4-4d1a-8172-06dcebac918c","limit":50} +2026-10-16T14:18:47.001Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"218ba7a6","correlationId":"16e06a83-70f4-4d1a-8172-06dcebac918c","count":0,"durationMs":2} +PASS src/store/event-registry.test.ts +2026-10-16T14:18:48.055Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:48.056Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:48.057Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} +2026-10-16T14:18:48.058Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:48.066Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:48.067Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:48.067Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:48.070Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:48.070Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:48.070Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:48.075Z info: Initiating graceful shutdown {"activeJobs":2} +2026-10-16T14:18:48.301Z info: Migration 005 (notification-expiration) applied successfully +2026-10-16T14:18:48.362Z info: Migration 005-repeat (notification-expiration) applied successfully +2026-10-16T14:18:48.390Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} +2026-10-16T14:18:48.403Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} +PASS src/migrations/005-notification-expiration.test.ts +PASS src/utils/config-snapshot.test.ts +PASS src/services/event-deduplication-service.test.ts +2026-10-16T14:18:49.084Z info: All jobs completed during graceful shutdown +2026-10-16T14:18:49.084Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:49.085Z info: Initiating graceful shutdown {"activeJobs":1} +2026-10-16T14:18:49.087Z info: Initiating graceful shutdown {"activeJobs":1} +2026-10-16T14:18:49.087Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} +2026-10-16T14:18:49.110Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T14:18:49.176Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T14:18:49.232Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T14:18:49.289Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T14:18:49.342Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T14:18:49.404Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T14:18:49.578Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.600Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.616Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.631Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.649Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.663Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.684Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.703Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.720Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.735Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.750Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.765Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.780Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.796Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.813Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.828Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.845Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.869Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.893Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.918Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.943Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.960Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.976Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:49.992Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.008Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/services/worker-manager.test.ts +2026-10-16T14:18:50.024Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.040Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.055Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.073Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.089Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.091Z info: All jobs completed during graceful shutdown +2026-10-16T14:18:50.092Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:50.093Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:50.093Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:50.094Z warn: Graceful shutdown already initiated +2026-10-16T14:18:50.094Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:50.095Z info: Worker manager force shutdown completed +2026-10-16T14:18:50.096Z info: Worker manager force shutdown completed +2026-10-16T14:18:50.106Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.118Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.134Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.149Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.165Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.181Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.196Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.212Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.228Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/utils/payload-size-validator.test.ts +2026-10-16T14:18:50.243Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.259Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.275Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.291Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.307Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.323Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.338Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/middleware/request-id.test.ts +2026-10-16T14:18:50.355Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.371Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.385Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.401Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.418Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.433Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.449Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.466Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.480Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.496Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.512Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.527Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.543Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T14:18:50.559Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/services/rpc-rate-limiter.test.ts +2026-10-16T14:18:50.575Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/services/job-monitor.test.ts +2026-10-16T14:18:50.777Z error: Job monitor: job failed {"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":0} +PASS src/store/preference-store.test.ts +PASS src/api/request-size-limiter.test.ts +PASS src/services/notification-stats-cache.test.ts +PASS src/services/providers/provider-health.test.ts +2026-10-16T14:18:52.733Z info: Notification analytics aggregator initialized with defaults +PASS src/utils/pagination.test.ts +PASS src/api/rate-limiter.test.ts +PASS src/__tests__/load-test-runner.test.ts +2026-10-16T14:18:53.624Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.642Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.643Z info: Database migrations completed +2026-10-16T14:18:53.643Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.648Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.666Z info: Database connection closed +2026-10-16T14:18:53.675Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.708Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.709Z info: Database migrations completed +2026-10-16T14:18:53.710Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.731Z info: Database connection closed +2026-10-16T14:18:53.733Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.744Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.745Z info: Database migrations completed +2026-10-16T14:18:53.745Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.748Z info: Database connection closed +2026-10-16T14:18:53.751Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.763Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.763Z info: Database migrations completed +2026-10-16T14:18:53.763Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.765Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.769Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} +2026-10-16T14:18:53.772Z info: Notification template updated {"templateId":"tmpl-001","actor":"admin@example.com"} +2026-10-16T14:18:53.778Z info: Database connection closed +2026-10-16T14:18:53.781Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.793Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.793Z info: Database migrations completed +2026-10-16T14:18:53.794Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.795Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.798Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} +2026-10-16T14:18:53.799Z info: Notification template updated {"templateId":"tmpl-001","actor":"editor-1"} +2026-10-16T14:18:53.801Z info: Template audit record created {"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} +2026-10-16T14:18:53.806Z info: Notification template updated {"templateId":"tmpl-001","actor":"editor-2"} +2026-10-16T14:18:53.810Z info: Database connection closed +2026-10-16T14:18:53.812Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.825Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.825Z info: Database migrations completed +2026-10-16T14:18:53.826Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.827Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.829Z info: Database connection closed +2026-10-16T14:18:53.831Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.840Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.841Z info: Database migrations completed +2026-10-16T14:18:53.841Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.842Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.844Z info: Database connection closed +2026-10-16T14:18:53.847Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.859Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.859Z info: Database migrations completed +2026-10-16T14:18:53.859Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.861Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.863Z info: Database connection closed +2026-10-16T14:18:53.865Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.877Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.878Z info: Database migrations completed +2026-10-16T14:18:53.878Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.879Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.881Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} +2026-10-16T14:18:53.883Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} +2026-10-16T14:18:53.884Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-001","deleted":1} +2026-10-16T14:18:53.885Z info: Notification template updated {"templateId":"tmpl-001","actor":"cache-admin"} +2026-10-16T14:18:53.888Z info: Database connection closed +2026-10-16T14:18:53.891Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.901Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.901Z info: Database migrations completed +2026-10-16T14:18:53.902Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.906Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.909Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} +2026-10-16T14:18:53.910Z info: Notification template updated {"templateId":"tmpl-001","actor":"auditor"} +2026-10-16T14:18:53.912Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T14:18:53.913Z info: Database connection closed +2026-10-16T14:18:53.915Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.926Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.926Z info: Database migrations completed +2026-10-16T14:18:53.927Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.927Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T14:18:53.929Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} +2026-10-16T14:18:53.930Z info: Notification template updated {"templateId":"tmpl-001","actor":"auditor"} +2026-10-16T14:18:53.931Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T14:18:53.932Z info: Database connection closed +2026-10-16T14:18:53.933Z info: Connected to SQLite database {"path":":memory:"} +PASS src/services/notification-template-repository.test.ts +2026-10-16T14:18:53.944Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.944Z info: Database migrations completed +2026-10-16T14:18:53.944Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.954Z info: Database connection closed +2026-10-16T14:18:53.957Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:53.965Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:53.966Z info: Database migrations completed +2026-10-16T14:18:53.966Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:53.967Z info: Database connection closed +PASS src/__tests__/api-contract.test.ts +2026-10-16T14:18:54.228Z info: Handling GET /api/events {"requestId":"9fce7c34","correlationId":"a8d95321-47a7-44ca-9473-31c02fa198ba","limit":20} +2026-10-16T14:18:54.230Z info: GET /api/events complete {"requestId":"9fce7c34","correlationId":"a8d95321-47a7-44ca-9473-31c02fa198ba","returned":0,"durationMs":2} +2026-10-16T14:18:54.249Z warn: Unhandled request {"requestId":"18811c4f","correlationId":"e0f533d1-8962-4728-89bd-2e8a402d52f4","method":"GET","url":"/api/definitely-not-a-route"} +2026-10-16T14:18:54.250Z info: Request completed {"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":2,"requestId":"18811c4f"} +2026-10-16T14:18:54.258Z info: Notification analytics aggregator initialized with defaults +2026-10-16T14:18:54.259Z info: Handling GET /api/analytics {"requestId":"cc0d1526","correlationId":"f297f7e9-5759-40c8-b424-132822c350d8","totalRecorded":0,"reset":false,"durationMs":1} +2026-10-16T14:18:54.262Z info: Handling GET /api/events {"requestId":"8ba282eb","correlationId":"30f9a245-5c5e-439e-a222-c2f643ae686a","limit":20} +2026-10-16T14:18:54.263Z info: GET /api/events complete {"requestId":"8ba282eb","correlationId":"30f9a245-5c5e-439e-a222-c2f643ae686a","returned":0,"durationMs":1} +PASS src/__tests__/load.test.ts +FAIL src/services/retry-scheduler-webhook.test.ts + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +PASS src/services/notification-search-service.test.ts +2026-10-16T14:18:54.566Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T14:18:54.569Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T14:18:54.575Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T14:18:54.581Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T14:18:54.585Z info: Notification search complete {"total":1,"returned":1,"limit":20,"offset":0} +PASS src/api/events-server.health.test.ts +PASS src/api/rate-limit-scenarios.test.ts +PASS src/services/dead-letter-manager.test.ts +PASS src/services/event-quarantine.test.ts +PASS src/services/notification-metrics-runner.test.ts +PASS src/services/provider-registry.test.ts +2026-10-16T14:18:55.387Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock Provider","capabilities":["RICH_FORMATTING"]} +2026-10-16T14:18:55.393Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing Mock","capabilities":[]} +2026-10-16T14:18:55.395Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial Mock","capabilities":["RICH_FORMATTING"]} +2026-10-16T14:18:55.473Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:55.482Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:55.483Z info: Database migrations completed +2026-10-16T14:18:55.484Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:55.488Z info: Notification template created {"templateId":"svc-template"} +PASS src/services/notification-template-service.test.ts +2026-10-16T14:18:55.489Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} +2026-10-16T14:18:55.494Z info: Template audit record created {"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} +2026-10-16T14:18:55.495Z info: [TemplateCache] Template invalidated {"templateId":"svc-template","deleted":1} +2026-10-16T14:18:55.496Z info: Notification template updated {"templateId":"svc-template","actor":"service-admin"} +2026-10-16T14:18:55.497Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} +2026-10-16T14:18:55.499Z info: Database connection closed +2026-10-16T14:18:55.501Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:55.510Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:55.510Z info: Database migrations completed +2026-10-16T14:18:55.510Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:55.512Z info: Notification template created {"templateId":"tmpl-a"} +2026-10-16T14:18:55.512Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} +2026-10-16T14:18:55.513Z info: Notification template created {"templateId":"tmpl-b"} +2026-10-16T14:18:55.513Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} +2026-10-16T14:18:55.514Z info: Database connection closed +2026-10-16T14:18:55.515Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:55.524Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:55.525Z info: Database migrations completed +2026-10-16T14:18:55.525Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:55.526Z info: Notification template created {"templateId":"del-tmpl"} +2026-10-16T14:18:55.526Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} +2026-10-16T14:18:55.527Z info: [TemplateCache] Template invalidated {"templateId":"del-tmpl","deleted":1} +2026-10-16T14:18:55.527Z info: Notification template deleted {"templateId":"del-tmpl"} +2026-10-16T14:18:55.527Z info: [TemplateCache] Template invalidated {"templateId":"del-tmpl","deleted":0} +2026-10-16T14:18:55.528Z info: Database connection closed +2026-10-16T14:18:55.530Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:55.538Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:55.539Z info: Database migrations completed +2026-10-16T14:18:55.539Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:55.540Z info: Notification template created {"templateId":"render-tmpl"} +2026-10-16T14:18:55.540Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} +2026-10-16T14:18:55.541Z info: Database connection closed +2026-10-16T14:18:55.543Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:55.550Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:55.551Z info: Database migrations completed +2026-10-16T14:18:55.551Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:55.552Z info: Notification template created {"templateId":"render-missing"} +2026-10-16T14:18:55.552Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} +2026-10-16T14:18:55.568Z info: Database connection closed +2026-10-16T14:18:55.571Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:55.579Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:55.579Z info: Database migrations completed +2026-10-16T14:18:55.579Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:55.580Z info: Notification template created {"templateId":"no-vars"} +2026-10-16T14:18:55.581Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} +2026-10-16T14:18:55.581Z info: Database connection closed +PASS src/utils/validation.test.ts +PASS src/utils/logger-structured.test.ts +PASS src/services/subscription-event-emitter.test.ts +PASS src/__tests__/shutdown-unit.test.ts +PASS src/utils/redact.test.ts +PASS src/services/notification-import-service.test.ts +2026-10-16T14:18:56.150Z info: Bulk notification import complete {"imported":2,"skipped":0,"total":2,"format":"json"} +2026-10-16T14:18:56.153Z info: Bulk notification import complete {"imported":1,"skipped":2,"total":3,"format":"json"} +2026-10-16T14:18:56.158Z info: Bulk notification import complete {"imported":1,"skipped":1,"total":2,"format":"csv"} +PASS src/services/notification-health-monitor.test.ts +PASS src/utils/log-level-resolver.test.ts +2026-10-16T14:18:56.283Z info: NotificationHealthMonitor started {"intervalMs":30000} +2026-10-16T14:18:56.286Z info: NotificationHealthMonitor stopped +PASS src/utils/payload-compression.test.ts +PASS src/utils/event-utils.test.ts +2026-10-16T14:18:56.445Z info: Payload compressed {"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} +2026-10-16T14:18:56.468Z warn: Payload decompression failed, returning original value {"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} +2026-10-16T14:18:56.471Z warn: Payload decompression failed, returning original value {"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} +PASS src/services/historical-backfill-service.test.ts +PASS src/api/events-server.history.test.ts +PASS src/services/notification-template-cache.test.ts +2026-10-16T14:18:56.988Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.990Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} +2026-10-16T14:18:56.991Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.992Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.993Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} +2026-10-16T14:18:56.994Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.995Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} +2026-10-16T14:18:56.995Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.996Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} +2026-10-16T14:18:56.997Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.997Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} +2026-10-16T14:18:56.998Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:56.999Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.000Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} +2026-10-16T14:18:57.000Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} +2026-10-16T14:18:57.001Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.001Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} +2026-10-16T14:18:57.002Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} +2026-10-16T14:18:57.002Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.002Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.003Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} +2026-10-16T14:18:57.004Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.005Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} +2026-10-16T14:18:57.006Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.007Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.008Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.008Z info: [TemplateCache] All templates invalidated +2026-10-16T14:18:57.009Z info: [TemplateCache] All templates invalidated +PASS src/services/retry-policy.test.ts +PASS src/config-logging.test.ts +PASS src/utils/batch-validator.test.ts +PASS src/services/event-subscriber-checkpoint.test.ts +PASS src/services/retry-statistics.test.ts +PASS src/middleware/security-headers.test.ts +PASS src/services/retry-scheduler.test.ts +2026-10-16T14:18:57.718Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:57.719Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:57.719Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:57.719Z info: Worker manager force shutdown completed +2026-10-16T14:18:57.722Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T14:18:57.723Z info: No active jobs - proceeding with shutdown +2026-10-16T14:18:57.724Z info: All jobs have completed - shutdown can proceed +2026-10-16T14:18:57.724Z info: Worker manager force shutdown completed +2026-10-16T14:18:57.727Z info: Worker manager force shutdown completed +2026-10-16T14:18:57.730Z info: Worker manager force shutdown completed +2026-10-16T14:18:57.732Z info: Worker manager force shutdown completed +2026-10-16T14:18:57.735Z info: Worker manager force shutdown completed +2026-10-16T14:18:57.737Z info: Worker manager force shutdown completed +PASS src/services/event-store-consistency.test.ts +PASS src/utils/ledger-range-validator.test.ts +2026-10-16T14:18:57.892Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:57.900Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:57.900Z info: Database migrations completed +2026-10-16T14:18:57.901Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:57.928Z info: Database connection closed +PASS src/middleware/body-limit.test.ts +PASS src/services/search-suggestion.test.ts +2026-10-16T14:18:58.072Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:58.081Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:58.082Z info: Database migrations completed +2026-10-16T14:18:58.082Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:58.130Z info: Database connection closed +PASS src/__tests__/integration.test.ts +PASS src/services/scheduled-notification-repository-cache.test.ts +2026-10-16T14:18:58.249Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:58.258Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:58.258Z info: Database migrations completed +2026-10-16T14:18:58.258Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:58.267Z warn: Event registry at capacity, evicting oldest events {"maxEvents":2,"evicted":1} +2026-10-16T14:18:58.270Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.275Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.277Z info: Notification marked as completed {"id":2} +2026-10-16T14:18:58.281Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.282Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.283Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.283Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.284Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.285Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.286Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.287Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.288Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.288Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.310Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.311Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.312Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.312Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.313Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T14:18:58.316Z info: Notification marked as completed {"id":13} +2026-10-16T14:18:58.317Z info: Notification marked as completed {"id":14} +2026-10-16T14:18:58.317Z info: Notification marked as completed {"id":15} +2026-10-16T14:18:58.328Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +PASS src/test-utils/notification-fixture-builder.test.ts +PASS src/utils/notification-version.test.ts +PASS src/api/__tests__/api-contract.test.ts +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/services/event-normalization.test.ts +PASS src/utils/cors-validator.test.ts +PASS src/api/api-version-registry.test.ts +PASS src/services/notification-api.test.ts +2026-10-16T14:18:59.147Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} +2026-10-16T14:18:59.179Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} +2026-10-16T14:18:59.180Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} +2026-10-16T14:18:59.192Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} +2026-10-16T14:18:59.195Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} +2026-10-16T14:18:59.201Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} +PASS src/services/notification-analytics-aggregator.test.ts +PASS src/services/event-provenance.test.ts +2026-10-16T14:18:59.551Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T14:18:59.564Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T14:18:59.564Z info: Database migrations completed +2026-10-16T14:18:59.565Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T14:18:59.592Z info: Database connection closed +PASS src/services/event-processing-queue.test.ts +PASS src/services/dns-rebinding-guard.test.ts +PASS src/services/webhook-replay-cache.test.ts (5.064 s) + +Summary of all failing tests +FAIL src/services/discord-notification-refactored.test.ts (86.777 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (33.391 s) + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 1 + + Object { + "alerts": true, + "audit": true, + - "ops": false, + + "ops": true, + } + +   296 | +  297 | const results = await deliverToAllChannels(createMockEvent({ +id: 'event-recover' }), 'req-recover'); + > 298 | expect(results).toEqual({ alerts: +true, ops: false, audit: true }); +  | ^ +  299 | +expect(channels[1].retryQueue.size()).toBe(1); +  300 | +  301 | // Advance past the base delay so the retry queue re-attempts 'ops'. + + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:298:23) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (26.449 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/services/concurrent-event-processing.test.ts (15.669 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/webhook-verifier.test.ts + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +FAIL src/api/queue-operational-metrics.test.ts (17.387 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + + ● Notification Queue Operational Metrics (#797) › Observability API Endpoints › GET /api/schedule/queue/metrics — +Dedicated Endpoint › supports aliases /api/schedule/queue-metrics and /api/notifications/queue-metrics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let testDbPath: string; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | testDbIndex++; +  77 | testDbPath = path.join( +  78 | __dirname, + + at src/api/queue-operational-metrics.test.ts:75:13 + at Object. (src/api/queue-operational-metrics.test.ts:68:9) + +FAIL src/services/retry-deduplication.test.ts (15.548 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/__tests__/stress.test.ts (205.296 s) + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + +FAIL src/services/scheduled-notification-queue.test.ts (10.513 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/api/execution-metrics-api.test.ts (6.186 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/database/query-performance.test.ts (8.415 s) + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   44 | }); +  45 | + > 46 | it('uses an indexed plan for pending claim queries', +async () => { +  | ^ +  47 | // Seed enough rows that a full scan would be meaningful +  48 | for (let i = 0; i < +200; i++) { +  49 | await db.run( + + at src/database/query-performance.test.ts:46:3 + at Object. (src/database/query-performance.test.ts:20:1) + +FAIL src/api/events-server.test.ts + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/__tests__/notification-flow-e2e.test.ts (18.577 s) + ● 1. Notification creation › rejects a duplicate idempotency key used with a different payload + + expect(received).rejects.toThrow(expected) + + Expected substring: "Idempotency key reused with different request body" + Received message: "Idempotency key reused with a different request body" + +   131 | if (!isValidRequest) { +  132 | logger.warn('Idempotency key reused with different request body +(replay suspected)', logMeta); + > 133 | throw new +IdempotencyKeyReuseError(); +  | ^ +  134 | } +  135 | +  136 | logger.info('Replay intercepted: returning cached idempotent +response', { + + at IdempotencyKeyService.processWithIdempotency (src/services/idempotency-key-service.ts:133:15) + at NotificationAPI.scheduleNotification (src/services/notification-api.ts:168:9) + at Object. (src/__tests__/notification-flow-e2e.test.ts:253:5) + at Object.toThrow (node_modules/expect/build/index.js:2155:20) + at Object. (src/__tests__/notification-flow-e2e.test.ts:259:15) + + ● 2. Delivery workflows › processes a due notification and transitions it to COMPLETED + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   343 | +  344 | const row = await repository.getById(id); + > 345 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  346 | expect(sendMock).toHaveBeenCalledTimes(1); +  347 | }); +  348 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:345:25) + + ● 2. Delivery workflows › delivers once executeAt elapses mid-run + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   377 | await jest.advanceTimersByTimeAsync(400); +  378 | + > 379 | expect((await repository.getById(id))!. +status).toBe(NotificationStatus.COMPLETED); +  | ^ +  380 | expect(sendMock).toHaveBeenCalledTimes(1); +  381 | }); +  382 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:379:52) + + ● 2. Delivery workflows › writes a SUCCESS execution log entry after delivery + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 0 + Received array: [] + +   396 | [id] +  397 | ); + > 398 | expect(logs).toHaveLength(1); +  | ^ +  399 | expect(logs[0].status).toBe('SUCCESS'); +  400 | expect(logs[0].execution_attempt).toBe(1); +  401 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:398:18) + + ● 2. Delivery workflows › processes a batch of multiple due notifications + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   418 | +  419 | for (const id of ids) { + > 420 | expect((await repository.getById(id))![33 +m.status).toBe(NotificationStatus.COMPLETED); +  | ^ +  421 | } +  422 | expect(sendMock).toHaveBeenCalledTimes(3); +  423 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:420:54) + + ● 2. Delivery workflows › forwards the event and contract config to the Discord service + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: ObjectContaining {"id": "deliver-evt-1"}, ObjectContaining {"address": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"}, Any + + Number of calls: 0 + +   438 | await jest.advanceTimersByTimeAsync(250); +  439 | + > 440 | expect(sendMock).toHaveBeenCalledWith( +  | ^ +  441 | expect.objectContaining({ id: 'deliver-evt-1' }), +  442 | expect.objectContaining({ address: contractConfig.address +}), +  443 | expect.any(String) + + at Object. (src/__tests__/notification-flow-e2e.test.ts:440:22) + + ● 3. Retry scenarios › prevents concurrent duplicate processing via distributed lock + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   587 | await p1; +  588 | + > 589 | expect(sendMock).toHaveBeenCalledTimes(1); +  | ^ +  590 | const row = await db.get<{ +status: string }>( +  591 | 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] +  592 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:589:22) + + ● 3. Retry scenarios › retries a failed delivery on the next scheduler poll cycle + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   620 | +  621 | const row = await repository.getById(id); + > 622 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  623 | expect(sendMock).toHaveBeenCalledTimes(2); +  624 | +  625 | await scheduler.stop(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:622:25) + + ● 4. Failure handling › marks a notification FAILED after exhausting all retries + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PROCESSING" + +   664 | 'SELECT status, retry_count FROM scheduled_notifications WHERE id = +?', [id] +  665 | ); + > 666 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  667 | }); +  668 | +  669 | it('logs a permanent-failure error after max retries are exhausted', +async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:666:25) + + ● 4. Failure handling › logs a permanent-failure error after max retries are exhausted + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"id": 2, "totalAttempts": 3} + Received: "Notification permanently failed after max retries", {"failureType": "unknown", "id": 2, "requestId": +"e2e-req-id", "totalAttempts": 4} + + Number of calls: 1 + +   677 | await retryScheduler.runOnce(); +  678 | + > 679 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  680 | 'Notification permanently failed after max retries', +  681 | expect.objectContaining({ id, totalAttempts: 3 }) +  682 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:679:26) + + ● 4. Failure handling › transitions scheduler-dispatched notification to FAILED when retries run out + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PENDING" + +   738 | +  739 | const row = await repository.getById(id); + > 740 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  741 | +  742 | await scheduler.stop(); +  743 | jest.useRealTimers(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:740:25) + + ● 4. Failure handling › does not recover a PROCESSING lock that has not yet expired + + SQLITE_ERROR: cannot start a transaction within a transaction + +FAIL src/services/execution-metrics.test.ts (9.89 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (12.255 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/tests/payload-integrity.test.ts + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +3761.04267570452, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T13:17:34.975Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/discord-notification.test.ts + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +FAIL src/api/template-routes.test.ts + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4611.300108539392, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T13:17:40.399Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/tests/template-api-integration.test.ts + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792070257374} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792156658473, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792070257374} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792156658473, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "784a2fcf"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/tests/api-versioning.test.ts + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "a9591165"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "f91a6f99-e1cd-4be0-895c-4a69878ec6b1", +"eventId": "event-1", "requestId": "f91a6f99-e1cd-4be0-895c-4a69878ec6b1"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 1, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/__tests__/scheduler-concurrency.test.ts (119.405 s) + ● Scheduler worker concurrency › concurrency larger than the batch is clamped to the batch size + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   123 | }); +  124 | + > 125 | test('concurrency larger than the batch is clamped to the batch +size', async () => { +  | ^ +  126 | await seedDueNotifications(2); +  127 | await sleep(1300); +  128 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:125:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +FAIL src/__tests__/database-recovery.test.ts (1612.13 s) + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   29 | }); +  30 | + > 31 | test('recovers transparently when the handle is closed +mid-flight', async () => { +  | ^ +  32 | const db = new Database(testDbPath, { +maxReconnectAttempts: 3, reconnectBaseDelayMs: 10 }); +  33 | await db.initialize(); +  34 | await db.run('CREATE TABLE t (id INTEGER PRIMARY KEY, v +TEXT)'); + + at src/__tests__/database-recovery.test.ts:31:3 + at Object. (src/__tests__/database-recovery.test.ts:18:1) + + ● Database connection recovery › recovers transparently when the handle is closed mid-flight + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › fails fast after the reconnect budget when the database cannot be re-opened + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › does not attempt recovery for data errors + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › no recovery inside a transaction: failure rolls back and propagates + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   22 | const dbDir = path.dirname(testDbPath); +  23 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 24 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  25 | }); +  26 | +  27 | afterEach(() => { + + at Object. (src/__tests__/database-recovery.test.ts:24:39) + + ● Database connection recovery › operations after an explicit close fail rather than silently reconnecting + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-db-recovery.db' + +   26 | +  27 | afterEach(() => { + > 28 | if (fs.existsSync(testDbPath)) +fs.unlinkSync(testDbPath); +  | ^ +  29 | }); +  30 | +  31 | test('recovers transparently when the handle is closed mid-flight', +async () => { + + at Object. (src/__tests__/database-recovery.test.ts:28:39) + +FAIL src/services/retry-scheduler-webhook.test.ts + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + + +Test Suites: 50 failed, 1 skipped, 101 passed, 151 of 152 total +Tests: 270 failed, 4 skipped, 2278 passed, 2552 total +Snapshots: 0 total +Time: 3943.306 s diff --git a/listener/_jest-clean.txt b/listener/_jest-clean.txt new file mode 100644 index 00000000..c64dff79 --- /dev/null +++ b/listener/_jest-clean.txt @@ -0,0 +1,21697 @@ + +> listener@1.0.0 test +> node ./node_modules/jest/bin/jest.js + +node.exe : PASS src/tests/notification-scheduler.test.ts (51.364 s) +At line:1 char:1 ++ & "C:\Program Files\nodejs/node.exe" "C:\Program Files\nodejs/node_mo ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (PASS src/tests/...t.ts (51.364 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ● Console + + console.log + 2026-10-16T23:42:36.866Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.809Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.817Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.843Z info: Database initialized successfully +{"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.932Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.020Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.045Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.135Z info: Processing scheduled notification +{"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.190Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":1284} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.501Z info: Notification marked as completed {"requestId":"test-request-id","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.519Z info: Notification delivered successfully +{"requestId":"test-request-id","id":1,"type":"webhook","durationMs":348} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.523Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-success-test","count":1,"durationMs":547} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.544Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.622Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.656Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.661Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.666Z info: Processing scheduled notification +{"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.668Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":1064} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.672Z error: Failed to process notification +{"requestId":"test-request-id","id":2,"error":{"message":"secret +token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.702Z error: Job monitor: job failed +{"jobId":"notification-2","jobName":"scheduled-notification","error":"secret token=should-not-persist","durationMs":36} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.745Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:42:44.722Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:39.794Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":168} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.861Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.002Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.149Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.209Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.277Z info: Expired notification skipped before delivery +{"requestId":"test-request-id","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.392Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.417Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.419Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.422Z info: Processing scheduled notification +{"requestId":"test-request-id","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:40.426Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":5,"executeAt":{},"now":{},"missedByMs":60055} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:40.429Z error: Failed to process notification +{"requestId":"test-request-id","id":5,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary +outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:40.431Z error: Job monitor: job failed +{"jobId":"notification-5","jobName":"scheduled-notification","error":"temporary outage","durationMs":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:40.447Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:42:40.532Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:40.472Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":77} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.769Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.802Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-expiration-test","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.845Z info: Expired retry skipped before delivery +{"requestId":"test-request-id","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.859Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.973Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.004Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.052Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.059Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.074Z info: Processing scheduled notification +{"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.086Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1130} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.141Z info: Notification marked as completed {"requestId":"test-request-id","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.169Z info: Notification delivered successfully +{"requestId":"test-request-id","id":6,"type":"discord","durationMs":33} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.176Z info: Processing scheduled notification +{"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.187Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":1209} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.237Z info: Notification marked as completed {"requestId":"test-request-id","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.285Z info: Notification delivered successfully +{"requestId":"test-request-id","id":7,"type":"discord","durationMs":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.288Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":277} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.315Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.445Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.477Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.501Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.601Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.626Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.629Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.634Z info: Processing scheduled notification +{"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.637Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60071} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.639Z error: Failed to process notification +{"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: +temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.641Z error: Job monitor: job failed +{"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.668Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:42:43.643Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:42:41.695Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":90} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.755Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.759Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.764Z info: Retrying notification +{"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.788Z info: Notification marked as completed {"requestId":"test-request-id","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.805Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.811Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.995Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.031Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.135Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.255Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.368Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.504Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.559Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.686Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.707Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.773Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.810Z warn: Notification moved to dead letter queue +{"id":15,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.819Z info: Notification marked for retry or failed +{"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.875Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.918Z info: Notification cancelled {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.036Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.062Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.157Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.189Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.308Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.352Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.689Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.706Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.759Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.803Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.938Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.989Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.037Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.057Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.401Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.405Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.407Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.440Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.477Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.615Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.642Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.695Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.744Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.747Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.799Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.821Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-stale"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.861Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.928Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.952Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.964Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/scheduled-notification-queue.test.ts (52.181 s) + ● Console + + console.log + 2026-10-16T23:42:31.392Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.642Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.723Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.936Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.523Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.579Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.676Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.936Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.952Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.975Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.107Z info: Fetched and locked pending notifications +{"count":1,"processorId":"restarted-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.129Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.229Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:36.957Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:36.980Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:36.991Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.130Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.192Z info: Fetched and locked pending notifications +{"count":1,"processorId":"doomed-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.273Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.288Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.312Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.318Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.321Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.426Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.616Z info: Fetched and locked pending notifications +{"count":1,"processorId":"next-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.642Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.683Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.225Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.233Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.358Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.469Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.489Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.518Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.534Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.540Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.563Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.568Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.570Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.576Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.588Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.795Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.818Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.829Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.883Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.909Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.937Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.973Z info: Fetched and locked pending notifications +{"count":3,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.995Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.020Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.372Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.377Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.420Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.468Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.505Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.535Z info: Fetched and locked pending notifications +{"count":2,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.706Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.721Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.745Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.119Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.144Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.168Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.255Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.287Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.372Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.384Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.393Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.774Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.778Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.791Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.819Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.846Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.868Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:43:45.850Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.936Z info: Fetched due retries {"count":1,"processorId":"retry-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.942Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.960Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.931Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.943Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.953Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.024Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.045Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.088Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent outage"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.099Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.138Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +PASS src/api/queue-visibility.test.ts (53.255 s) + ● Console + + console.log + 2026-10-16T23:42:28.873Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:30.619Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:30.626Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:30.648Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:30.760Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:30.808Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:30.838Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:31.127Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:31.157Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.122Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.168Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.177Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.376Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:32.611Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.635Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:33.920Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.035Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.111Z info: Handling GET /api/schedule/queue +{"requestId":"da8e4509","correlationId":"1463b695-1505-407e-bd4a-642905135894","count":0,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.356Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:34.443Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.572Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.585Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.589Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.669Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.688Z info: Handling GET /api/schedule/queue +{"requestId":"006fba70","correlationId":"b8434abd-4fbf-4688-9809-3d0ec03e9ed1","count":1,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.736Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:37.772Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.273Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.376Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.423Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.478Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.506Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.529Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.567Z warn: Notification moved to dead letter queue +{"id":2,"notificationType":"discord","failureReason":"oops"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.572Z info: Notification marked for retry or failed +{"id":2,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.592Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.603Z info: Handling GET /api/schedule/queue +{"requestId":"3a5c92c0","correlationId":"7ff47e29-0c1f-43b8-97cb-b3329a2889e9","count":1,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.616Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.628Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.805Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.818Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.835Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.884Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.916Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.961Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.004Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.037Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.053Z info: Handling GET /api/schedule/queue +{"requestId":"f3dfe731","correlationId":"5d8ff043-7bd9-43da-bb0a-f1ee16730fae","count":3,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.074Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.135Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.421Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.438Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.455Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.494Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.541Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.950Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.977Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.000Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.043Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.110Z info: Handling GET /api/schedule/queue +{"requestId":"37212977","correlationId":"7df67581-c517-41bd-98a7-670beaddcdbc","count":1,"durationMs":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.195Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.227Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.592Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.597Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.601Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.631Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.641Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.790Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.799Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.814Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.834Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.867Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.886Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.891Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM +dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.898Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibi +lity.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.930Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.040Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.070Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.843Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.870Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.877Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.963Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.035Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.075Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.087Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM +dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.094Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibi +lity.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/concurrent-event-processing.test.ts (55.885 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/retry-deduplication.test.ts (63.88 s) + ● Console + + console.log + 2026-10-16T23:42:36.789Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.672Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.682Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.700Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.738Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.821Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.915Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:38.923Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.376Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Final failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.427Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.488Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:39.626Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.824Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.828Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.842Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.870Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.918Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:40.935Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:41.085Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.428Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.439Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.442Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.476Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.567Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.701Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Second attempt failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.704Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.753Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.769Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.889Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.259Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.268Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.272Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.341Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.408Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.445Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.527Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.613Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.643Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.738Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.814Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.869Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.902Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.968Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.032Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.128Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.133Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.263Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.271Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.311Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.389Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.433Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.513Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.565Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.591Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.639Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.694Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.746Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.782Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.824Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.870Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.946Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.949Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.021Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.029Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.058Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.119Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.148Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.209Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.318Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.433Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.607Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.832Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.914Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.943Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.018Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.081Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.175Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.181Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.251Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.263Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.540Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.676Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.706Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.855Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.942Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.004Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.122Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.173Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.312Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.445Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.524Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.594Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.706Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.709Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.803Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.809Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.839Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.886Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.927Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.156Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.233Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.262Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.374Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.474Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.492Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[19,2,"RETRY","Attempt 2 +failed",null,700],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.625Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.766Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.771Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.784Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[19,2,"RETRY","Attempt 2 +failed",null,700],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.841Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.518Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.527Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.540Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.562Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.676Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.701Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.717Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.752Z info: Fetched and locked pending notifications +{"count":1,"processorId":"test-processor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.785Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.942Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.985Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.994Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.014Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.041Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.093Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.125Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.157Z info: Notification cancelled {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.179Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.310Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.351Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.355Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.369Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.437Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.469Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.628Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.756Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.787Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.791Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.793Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.811Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.834Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.900Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.969Z info: Notification marked as completed {"id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:51.988Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.124Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.158Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.160Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.161Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.188Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.271Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.561Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.774Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":3,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.841Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":4,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.908Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":5,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.943Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":6,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:52.974Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":7,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.019Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":8,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.083Z warn: Notification moved to dead letter queue +{"id":9,"notificationType":"discord","failureReason":"Attempt 9 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.089Z info: Notification marked for retry or failed +{"id":9,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.154Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.220Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.413Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.476Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.488Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.490Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.564Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.672Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.717Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.861Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:53.899Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:54.238Z info: Notification marked as completed {"id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:54.290Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:54.595Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:54.758Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:55.056Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:55.359Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:55.808Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:55.911Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:55.943Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.074Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.183Z info: Notification marked as completed {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.245Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.325Z info: Notification marked for retry or failed +{"id":17,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.407Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.457Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.520Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.594Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.692Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.790Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.842Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.920Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.957Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:56.973Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.028Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.064Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.110Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.124Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.185Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.189Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:57.204Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (64.41 s) + ● Console + + console.log + 2026-10-16T23:42:42.329Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.458Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.462Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.478Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.651Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.656Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.660Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.665Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.894Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.903Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.911Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:42.918Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.235Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.243Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.270Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.278Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.862Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.870Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.876Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.881Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.210Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.221Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.243Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.262Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.677Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.696Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.708Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.710Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.778Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.787Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.790Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:58.795Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/services/execution-metrics.test.ts (11.86 s) + ● Console + + console.log + 2026-10-16T23:42:59.852Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.677Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.686Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.702Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.757Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.796Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.835Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.870Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.886Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.995Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.807Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.809Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.812Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.903Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.262Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.405Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.493Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.557Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.589Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.741Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.920Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:02.932Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.173Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.177Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.228Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.405Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.478Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.491Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.528Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.655Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.892Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.961Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.103Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.224Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.405Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.477Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.494Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.580Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.796Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.011Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.175Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.309Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.337Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.394Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.456Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.458Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.590Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.591Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.605Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.729Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.535Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.537Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.539Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.552Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.576Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.592Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.620Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.638Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.687Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.692Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.703Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.923Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.901Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.908Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.910Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.205Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.374Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.196Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.242Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.292Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.386Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.419Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.458Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.474Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/api/queue-operational-metrics.test.ts (17.115 s) + ● Console + + console.log + 2026-10-16T23:42:58.801Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194178338-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:59.613Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:59.684Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:59.769Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194178338-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:59.862Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:59.894Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194179888-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.675Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.678Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.679Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194179888-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.725Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.762Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.789Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:00.795Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194180792-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.510Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.522Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.524Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194180792-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.639Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.728Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.814Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:01.879Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194181820-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.357Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.364Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.366Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194181820-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.408Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.446Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.524Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:03.594Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194183572-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.890Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.893Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.896Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194183572-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.954Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.027Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.484Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.488Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.783Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.010Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194185905-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.628Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.639Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.642Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194185905-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.657Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:06.678Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194186675-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.474Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.476Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.478Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194186675-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.502Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.519Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.539Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.625Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"b13533d8","correlationId":"028aaf45-f276-42d4-b3eb-762cb8b08c6e","durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.691Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:07.712Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194187696-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.577Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.585Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.606Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194187696-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.665Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"c2312344","correlationId":"23371636-e05a-4c7c-ab72-2dde6636367e","durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.675Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"b9e0e24e","correlationId":"bea01531-e4a8-4134-b5c9-ec0b2dec89bd","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.706Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:08.721Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194188712-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.406Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.408Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.410Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194188712-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.503Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:09.558Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194189537-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.442Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.445Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.458Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194189537-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.477Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.579Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:10.594Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194190589-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.285Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.293Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.295Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194190589-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.329Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.384Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.426Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:11.458Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +PASS src/__tests__/notification-flow-e2e.test.ts (57.231 s) + ● Console + + console.log + 2026-10-16T23:42:42.003Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.043Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:43.356Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.019Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:44.444Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:45.892Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.071Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:46.472Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:47.240Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:48.110Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.756Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:49.945Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.208Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.655Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:42:50.883Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/tests/payload-integrity.test.ts + ● Console + + console.log + 2026-10-16T23:43:03.343Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.458Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.532Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.621Z info: Database initialized successfully {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.672Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.835Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:04.987Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.029Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.410Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.460Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.620Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.660Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:05.829Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/services/discord-notification.test.ts (18.095 s) + ● DiscordNotificationService › sendEventNotification › should send event notification successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   58 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  59 | + > 60 | expect(result).toBe(true); +  | ^ +  61 | expect(mockFetch).toHaveBeenCalledTimes(1); +  62 | const [url, options] = +mockFetch.mock.calls[0]; +  63 | expect(url).toBe(mockConfig.webhookUrl); + + at Object. (src/services/discord-notification.test.ts:60:22) + + ● DiscordNotificationService › sendEventNotification › should handle request timeout + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Discord webhook request timed out", ObjectContaining {"timeoutMs": 100, "webhookId": "123"} + Received + 1: "Discord webhook request error", {"attempt": 0, "contractAddress": "CA123", "correlationId": +"ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", "durationMs": 349, "error": [DnsRebindingError: DNS resolution returned no +addresses for hostname: discord.com], "eventId": "event-123", "requestId": "ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", +"webhookId": "123"} + 2: "Exceeded max Discord retry attempts", {"contractAddress": "CA123", "correlationId": +"ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", "eventId": "event-123", "maxRetries": 0, "requestId": +"ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", "webhookId": "123"} + + Number of calls: 2 + +   111 | +  112 | expect(result).toBe(false); + > 113 | expect(mockLoggerModule.error).toHaveBeenCalledWith( +  | ^ +  114 | 'Discord webhook request timed out', +  115 | expect.objectContaining({ +  116 | webhookId: mockConfig.webhookId, + + at Object. (src/services/discord-notification.test.ts:113:38) + + ● DiscordNotificationService › duplicate detection › skips the webhook call for a duplicate event + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   133 | const secondResult = await +service.sendEventNotification(mockEvent, mockContractConfig); +  134 | + > 135 | +expect(mockFetch).toHaveBeenCalledTimes(1); +  | ^ +  136 | expect(secondResult).toBe(true); +  137 | expect(service.getDeduplicationMetrics()).toEqual( +  138 | expect.objectContaining({ skippedDuplicates: 1, +cacheSize: 1 }), + + at Object. (src/services/discord-notification.test.ts:135:25) + + ● DiscordNotificationService › duplicate detection › logs a duplicate detection event + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping duplicate notification", ObjectContaining {"contractAddress": "CA123", "eventId": +"event-dup-log"} + Received + 1: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"07b1bfbc-02de-4047-953b-a0b67cc8bdae", "eventId": "event-dup-log", "requestId": +"07b1bfbc-02de-4047-953b-a0b67cc8bdae", "webhookId": "123"} + 2: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"bd2c1bf6-fb61-4f33-a8e8-57dc5866f05d", "eventId": "event-dup-log", "requestId": +"bd2c1bf6-fb61-4f33-a8e8-57dc5866f05d", "webhookId": "123"} + + Number of calls: 2 + +   151 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  152 | + > 153 | expect(mockLoggerModule.info).toHaveBeenCalledWith( +  | ^ +  154 | 'Skipping duplicate notification', +  155 | expect.objectContaining({ +  156 | eventId: 'event-dup-log', + + at Object. (src/services/discord-notification.test.ts:153:37) + + ● DiscordNotificationService › duplicate detection › allows the same notification request after the configured +window expires + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   173 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  174 | + > 175 | expect(result).toBe(true); +  | ^ +  176 | expect(mockFetch).toHaveBeenCalledTimes(2); +  177 | expect(service.getDeduplicationMetrics()).toEqual( +  178 | expect.objectContaining({ acceptedRequests: 2, +skippedDuplicates: 1 }), + + at Object. (src/services/discord-notification.test.ts:175:22) + + ● DiscordNotificationService › duplicate detection › allows the same event id on a different contract through + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 0 + +   191 | const result = await +service.sendEventNotification(mockEvent, contractB); +  192 | + > 193 | +expect(mockFetch).toHaveBeenCalledTimes(2); +  | ^ +  194 | expect(result).toBe(true); +  195 | }); +  196 | + + at Object. (src/services/discord-notification.test.ts:193:25) + + ● DiscordNotificationService › duplicate detection › does not mark an event as sent when the webhook call fails + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   213 | +  214 | expect(firstResult).toBe(false); + > 215 | expect(secondResult).toBe(true); +  | ^ +  216 | expect(mockFetch).toHaveBeenCalledTimes(2); +  217 | }); +  218 | + + at Object. (src/services/discord-notification.test.ts:215:28) + + ● DiscordNotificationService › duplicate detection › accepts an injected deduplicator + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   227 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  228 | + > 229 | +expect(deduplicator.size()).toBe(1); +  | ^ +  230 | +expect(deduplicator.isDuplicate(generateFingerprint('event-injected', +'CA123'))).toBe(true); +  231 | }); +  232 | }); + + at Object. (src/services/discord-notification.test.ts:229:35) + + ● DiscordNotificationService › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   239 | const result = await +service.sendTestMessage(); +  240 | + > 241 | expect(result).toBe(true); +  | ^ +  242 | }); +  243 | +  244 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification.test.ts:241:22) + + ● DiscordNotificationService › formatEventMessage › should format event with string value correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   264 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  265 | + > 266 | expect(result).toBe(true); +  | ^ +  267 | const [, options] = +mockFetch.mock.calls[0]; +  268 | const body = +JSON.parse(options.body); +  269 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:266:22) + + ● DiscordNotificationService › formatEventMessage › should truncate long string values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   283 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  284 | + > 285 | expect(result).toBe(true); +  | ^ +  286 | const [, options] = +mockFetch.mock.calls[0]; +  287 | const body = +JSON.parse(options.body); +  288 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:285:22) + + ● DiscordNotificationService › formatEventMessage › should handle symbol type values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   302 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  303 | + > 304 | expect(result).toBe(true); +  | ^ +  305 | const [, options] = +mockFetch.mock.calls[0]; +  306 | const body = +JSON.parse(options.body); +  307 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:304:22) + + ● DiscordNotificationService › formatEventMessage › should preserve short messages without truncation + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   320 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  321 | + > 322 | expect(result).toBe(true); +  | ^ +  323 | const [, options] = +mockFetch.mock.calls[0]; +  324 | const body = +JSON.parse(options.body); +  325 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:322:22) + + ● DiscordNotificationService › formatEventMessage › should truncate oversized embed titles + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   340 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  341 | + > 342 | expect(result).toBe(true); +  | ^ +  343 | const [, options] = +mockFetch.mock.calls[0]; +  344 | const body = +JSON.parse(options.body); +  345 | expect(body.embeds[0].title.length).toBeL +essThanOrEqual(256); + + at Object. (src/services/discord-notification.test.ts:342:22) + + ● DiscordNotificationService › formatEventMessage › should enforce total embed size limit + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   361 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  362 | + > 363 | expect(result).toBe(true); +  | ^ +  364 | const [, options] = +mockFetch.mock.calls[0]; +  365 | const body = +JSON.parse(options.body); +  366 | const embed = body.embeds[0]; + + at Object. (src/services/discord-notification.test.ts:363:22) + + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   380 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  381 | + > 382 | expect(result).toBe(true); +  | ^ +  383 | const [, options] = +mockFetch.mock.calls[0]; +  384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:382:22) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone +embedded in an scvString payload + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   459 | +  460 | describe('integration: mention in event string value reaches embed +sanitized', () => { + > 461 | it('strips @everyone embedded in an scvString +payload', async () => { +  | ^ +  462 | mockFetch.mockResolvedValueOnce({ ok: true }); +  463 | +  464 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:461:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone in +event topic symbol used as embed title + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   487 | }); +  488 | + > 489 | it('strips @everyone in event topic symbol used as embed +title', async () => { +  | ^ +  490 | mockFetch.mockResolvedValueOnce({ ok: true }); +  491 | +  492 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:489:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 0, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › tracks timeoutCount in DiscordNotificationService on +AbortError + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 1 + Received: 0 + +   71 | const success = await +service.sendTestMessage(); +  72 | expect(success).toBe(false); + > 73 | +expect(service.getMetrics().timeoutCount).toBeGreaterThanOrEqual(1); +  | ^ +  74 | }); +  75 | }); +  76 | + + at Object. (src/services/notification-failure-scenarios.test.ts:73:49) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/services/retry-scheduler-webhook.test.ts + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792107798043} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792194199194, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792107798043} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792194199195, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "ca93f78a"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "2ce0d7c3"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "f832376c-cff6-4b14-bddf-1dc671f56ff0", +"eventId": "event-1", "requestId": "f832376c-cff6-4b14-bddf-1dc671f56ff0"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +PASS src/__tests__/scheduler-concurrency.test.ts (9.862 s) + ● Console + + console.log + 2026-10-16T23:43:11.994Z info: Connected to SQLite database +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:12.944Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:12.951Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:12.967Z info: Database initialized successfully +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:12.984Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:13.040Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:13.051Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:14.625Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:14.627Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:14.843Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:14.846Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:14.896Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.253Z info: Fetched and locked pending notifications +{"requestId":"2caa9808","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.271Z info: Processing batch of scheduled notifications +{"requestId":"2caa9808","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.279Z info: Processing scheduled notification +{"requestId":"2caa9808","id":1,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.386Z info: Notification marked as completed {"requestId":"2caa9808","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.422Z info: Notification delivered successfully +{"requestId":"2caa9808","id":1,"type":"discord","durationMs":92} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.427Z info: Processing scheduled notification +{"requestId":"2caa9808","id":2,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.538Z info: Notification marked as completed {"requestId":"2caa9808","id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.575Z info: Notification delivered successfully +{"requestId":"2caa9808","id":2,"type":"discord","durationMs":91} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.578Z info: Processing scheduled notification +{"requestId":"2caa9808","id":3,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.719Z info: Notification marked as completed {"requestId":"2caa9808","id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.740Z info: Notification delivered successfully +{"requestId":"2caa9808","id":3,"type":"discord","durationMs":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.743Z info: Processing scheduled notification +{"requestId":"2caa9808","id":4,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.864Z info: Notification marked as completed {"requestId":"2caa9808","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.892Z info: Notification delivered successfully +{"requestId":"2caa9808","id":4,"type":"discord","durationMs":97} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:16.895Z info: Scheduler batch complete +{"requestId":"2caa9808","processorId":"test-uuid-0001","count":4,"durationMs":670} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.899Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.951Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.966Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.969Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.982Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.985Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.998Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:17.004Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:17.033Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:17.037Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:17.060Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:17.066Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:17.079Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:18.497Z info: Fetched and locked pending notifications +{"requestId":"b20efc6e","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:18.503Z info: Processing batch of scheduled notifications +{"requestId":"b20efc6e","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:18.515Z info: Processing scheduled notification +{"requestId":"b20efc6e","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:18.517Z info: Processing scheduled notification +{"requestId":"b20efc6e","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:18.519Z info: Processing scheduled notification +{"requestId":"b20efc6e","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:43:18.660Z info: Notification marked as completed {"requestId":"b20efc6e","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:18.662Z info: Notification marked as completed {"requestId":"b20efc6e","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:18.664Z info: Notification marked as completed {"requestId":"b20efc6e","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:43:18.713Z info: Notification delivered successfully +{"requestId":"b20efc6e","id":6,"type":"discord","durationMs":91} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:18.717Z info: Processing scheduled notification +{"requestId":"b20efc6e","id":8,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:18.719Z info: Notification delivered successfully +{"requestId":"b20efc6e","id":5,"type":"discord","durationMs":89} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:18.722Z info: Processing scheduled notification +{"requestId":"b20efc6e","id":9,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:18.725Z info: Notification delivered successfully +{"requestId":"b20efc6e","id":7,"type":"discord","durationMs":90} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:43:18.727Z info: Processing scheduled notification +{"requestId":"b20efc6e","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:43:18.852Z info: Notification marked as completed {"requestId":"b20efc6e","id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:18.855Z info: Notification marked as completed {"requestId":"b20efc6e","id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:18.857Z info: Notification marked as completed {"requestId":"b20efc6e","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:43:18.870Z info: Notification delivered successfully +{"requestId":"b20efc6e","id":9,"type":"discord","durationMs":92} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:18.925Z info: Notification delivered successfully +{"requestId":"b20efc6e","id":8,"type":"discord","durationMs":97} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:18.928Z info: Notification delivered successfully +{"requestId":"b20efc6e","id":10,"type":"discord","durationMs":87} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:43:18.930Z info: Scheduler batch complete +{"requestId":"b20efc6e","processorId":"test-uuid-0002","count":6,"durationMs":517} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:18.943Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:18.999Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.019Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.022Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.036Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.400Z info: Fetched and locked pending notifications +{"requestId":"2eceaa64","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.404Z info: Processing batch of scheduled notifications +{"requestId":"2eceaa64","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.408Z info: Processing scheduled notification +{"requestId":"2eceaa64","id":11,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:20.411Z info: Processing scheduled notification +{"requestId":"2eceaa64","id":12,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:20.513Z info: Notification marked as completed {"requestId":"2eceaa64","id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:20.609Z info: Notification marked as completed {"requestId":"2eceaa64","id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:20.616Z info: Notification delivered successfully +{"requestId":"2eceaa64","id":11,"type":"discord","durationMs":84} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:20.676Z info: Notification delivered successfully +{"requestId":"2eceaa64","id":12,"type":"discord","durationMs":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:43:20.690Z info: Scheduler batch complete +{"requestId":"2eceaa64","processorId":"test-uuid-0003","count":2,"durationMs":335} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.705Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.715Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/tests/api-versioning.test.ts + ● Console + + console.log + 2026-10-16T23:43:20.436Z info: Handling GET /api/events +{"requestId":"79c3612d","correlationId":"1264f3b3-a427-4cf7-893f-cb48d478abbd","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.450Z info: GET /api/events complete +{"requestId":"79c3612d","correlationId":"1264f3b3-a427-4cf7-893f-cb48d478abbd","returned":0,"durationMs":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.462Z info: Handling GET /api/events +{"requestId":"5357794a","correlationId":"1a89cd79-8b80-4329-b4ca-9cec1158c581","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.469Z info: GET /api/events complete +{"requestId":"5357794a","correlationId":"1a89cd79-8b80-4329-b4ca-9cec1158c581","returned":0,"durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.484Z info: Handling GET /api/events +{"requestId":"8fdf51e1","correlationId":"a5cd9543-f852-4eff-a55e-5324f0945a33","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.486Z info: GET /api/events complete +{"requestId":"8fdf51e1","correlationId":"a5cd9543-f852-4eff-a55e-5324f0945a33","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.492Z info: Handling GET /api/events +{"requestId":"cd06cbd7","correlationId":"d93648fb-7355-4858-af2d-f54b52080a26","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.497Z info: GET /api/events complete +{"requestId":"cd06cbd7","correlationId":"d93648fb-7355-4858-af2d-f54b52080a26","returned":0,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.503Z info: Handling GET /api/events +{"requestId":"86ba9e74","correlationId":"490837e8-e8d4-4b9f-92ab-00a28cd07cce","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.505Z info: GET /api/events complete +{"requestId":"86ba9e74","correlationId":"490837e8-e8d4-4b9f-92ab-00a28cd07cce","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.540Z info: Handling GET /api/events +{"requestId":"2f83d075","correlationId":"c2cff66d-a522-48e6-9f2f-fa16deae782b","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.544Z info: GET /api/events complete +{"requestId":"2f83d075","correlationId":"c2cff66d-a522-48e6-9f2f-fa16deae782b","returned":0,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.559Z info: Handling GET /api/events +{"requestId":"45676ec9","correlationId":"b638ad75-2f01-4001-921c-f0c052ee6a54","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.568Z info: GET /api/events complete +{"requestId":"45676ec9","correlationId":"b638ad75-2f01-4001-921c-f0c052ee6a54","returned":0,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.603Z info: Handling GET /api/events +{"requestId":"87338d4f","correlationId":"ed97ccea-4409-464e-a3b5-695c1e0d61ba","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.606Z info: GET /api/events complete +{"requestId":"87338d4f","correlationId":"ed97ccea-4409-464e-a3b5-695c1e0d61ba","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.615Z info: Handling GET /api/events +{"requestId":"e116076c","correlationId":"9939860f-b09a-4d6b-8c87-8ca2e2af239b","limit":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.618Z info: GET /api/events complete +{"requestId":"e116076c","correlationId":"9939860f-b09a-4d6b-8c87-8ca2e2af239b","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.655Z warn: Unhandled request {"requestId":"35d71949","correlationId":"cf9bd519-dbbe +-45d9-a97d-45047f80ebba","method":"GET","url":"/api/v1/unknown-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.676Z info: Request completed +{"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":25,"requestId":"35d71949"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.717Z warn: Unhandled request {"requestId":"c0191bd8","correlationId":"bb4c16f8-3da8 +-462f-8a0e-df18a6cfc77e","method":"GET","url":"/api/v1/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.719Z info: Request completed +{"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":3,"requestId":"c0191bd8"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (86.693 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   169 | +  170 | describe('fan-out delivery', () => { + > 171 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  172 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  173 | +  174 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:171:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   178 | }); +  179 | + > 180 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  181 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  182 | +  183 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:180:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   194 | +  195 | describe('channel-specific payloads', () => { + > 196 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  197 | await deliverToAllChannels( +  198 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  199 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:196:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   210 | }); +  211 | + > 212 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  213 | await deliverToAllChannels( +  214 | createMockEvent({ id: 'event-system', type: +'system' }) +  215 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:212:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +PASS src/database/query-performance.test.ts (6.147 s) + ● Console + + console.log + 2026-10-16T23:43:15.468Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792194195462.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.221Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.236Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:16.252Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792194195462.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.173Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/api/events-server.test.ts + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/webhook-verifier.test.ts + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/archive.test.ts + ● Console + + console.log + 2026-10-16T23:43:21.923Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T2 +3:43:21.921Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.957Z info: ArchiveService: cycle complete +{"archived":2,"purged":0,"durationMs":36,"jobId":"archive-cycle-1792194201920"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.960Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.975Z info: ArchiveService: archived processed notification immediately {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.982Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":3,"jobId":"archive-cycle-1792194201979"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.986Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.990Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792194201989"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.992Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.003Z info: ArchiveService: purged expired archive records +{"count":2,"cutoff":"2026-07-18T23:43:22.002Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.005Z info: ArchiveService: cycle complete +{"archived":0,"purged":2,"durationMs":3,"jobId":"archive-cycle-1792194202001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.008Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.012Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":0,"jobId":"archive-cycle-1792194202011"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.018Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.021Z info: ArchiveService started +{"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.021Z info: Waiting for active archival jobs to complete {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.021Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.036Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":14,"jobId":"archive-cycle-1792194202021"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.037Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.043Z info: Handling GET /api/archive {"requestId":"req-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.059Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.070Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.077Z info: Handling POST /api/archive/run {"requestId":"req-6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/api/execution-metrics-api.test.ts (6.544 s) + ● Console + + console.log + 2026-10-16T23:43:18.390Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.090Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.116Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.198Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.282Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.350Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.387Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.428Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.786Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:19.817Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.804Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.811Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.821Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.859Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.922Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.951Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.005Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.056Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.078Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.123Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.164Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.167Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.203Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.205Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.235Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.242Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.010Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.011Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.013Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.053Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.059Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.770Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.772Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.775Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.793Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.801Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.383Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.385Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.387Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.403Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.415Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.437Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.458Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Console + + console.log + 2026-10-16T23:43:22.700Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.739Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.743Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.747Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:22.749Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/tests/template-api-integration.test.ts + ● Console + + console.log + 2026-10-16T23:43:19.654Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.704Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.719Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.745Z info: Database initialized successfully {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.753Z error: Database run error {"sql":"duplicate inserts with the same key are +silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.810Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue +(\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n +notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n +target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details +TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count +INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES +scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.859Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS +update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n +UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.871Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.888Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE +notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.891Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.908Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.924Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.936Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:20.944Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.033Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.042Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.051Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.061Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.071Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.077Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.088Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.092Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.101Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.106Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.110Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.114Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.120Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.125Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.132Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.141Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.145Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.151Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.155Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.159Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.162Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.168Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.171Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.175Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.184Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.188Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.191Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.194Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.200Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.203Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.208Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.212Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.217Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:21.224Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/api/archive-api.test.ts + ● Console + + console.log + 2026-10-16T23:43:23.609Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.633Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an +integer, received 'abc'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.641Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.644Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= +100"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.650Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.653Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= +0"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.671Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.673Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be +one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.677Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.680Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be +a valid date, received 'not-a-date'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.684Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.688Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.695Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.701Z info: Handling POST /api/archive/run {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/services/notification-api-webhook.test.ts + ● Console + + console.log + 2026-10-16T23:43:23.800Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.823Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.829Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.832Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.835Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.848Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.851Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.855Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:23.859Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 5828.474759042641, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:43:29.910Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/api/template-routes.test.ts + ● Console + + console.log + 2026-10-16T23:43:24.630Z error: Failed to create template {"error":{"message":"uniqueKey: is required +and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is +required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: uniqueKey: is required and +must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required +and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:85:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.644Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:107:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.650Z error: Failed to create template {"error":{"message":"Invalid JSON +body","name":"Error","stack":"Error: Invalid JSON body\n at EventEmitter. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:42:16)\n at +EventEmitter.emit (node:events:509:28)\n at C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:26:9\n at processTicksAndRejections +(node:internal/process/task_queues:85:11)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.655Z error: Failed to create template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:141:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:137:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.661Z error: Failed to create template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:150:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:153:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.676Z error: Failed to list templates {"error":{"message":"channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.686Z info: Listed templates via API +{"requestId":"req-1","count":0,"channelType":"EMAIL","activeOnly":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.696Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:201:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.704Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:215:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.709Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:288:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:226:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.715Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:237:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.722Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:247:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.727Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:360:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:261:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.734Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:24.746Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/database/event-notification-indexes.test.ts + ● Console + + console.log + 2026-10-16T23:43:24.725Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-3512-1792194204715.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.408Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.412Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.439Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-3512-1792194204715.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.454Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, +created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILE +D","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.475Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● Console + + console.log + 2026-10-16T23:43:25.178Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.226Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.292Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.322Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.389Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +3849.7143791391845, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:43:29.102Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/tests/template-system.test.ts + ● Console + + console.log + 2026-10-16T23:43:24.786Z info: Connected to SQLite database {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.486Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.489Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.502Z info: Database initialized successfully {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.509Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX +IF NOT EXISTS idx_templates_unique_key \r\n ON +notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:25.535Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +PASS src/services/payload-validation.integration.test.ts +PASS src/api/__tests__/api-contract.test.ts +PASS src/services/stellar-rpc-manager.test.ts +PASS src/utils/validation.test.ts +PASS src/services/indexing-reconciliation-engine.test.ts +PASS src/config-schema.test.ts +PASS src/api/delivery-metrics-api.test.ts + ● Console + + console.log + 2026-10-16T23:43:27.189Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"ae7c3ec0" +,"correlationId":"d9f60090-305b-4731-a662-d6bc1bc777a9","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.248Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"4e8bf50c" +,"correlationId":"beb2255f-f303-48c1-869e-ec1c5d15f32e","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.264Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"02e02474" +,"correlationId":"480c636b-c459-4adf-ab11-25d4c412167e","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs" +:1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.267Z info: Delivery metrics window reset after read +{"requestId":"02e02474","correlationId":"480c636b-c459-4adf-ab11-25d4c412167e"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.281Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"f2eeace1" +,"correlationId":"ddda4bfa-6739-456e-9985-95aab1b0edf6","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.299Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"31ace6ac" +,"correlationId":"4dfc60e1-e3d1-47e7-b17b-f7661cd51664","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.314Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"7112ab05","correlationId":"7a30aae2-0dd7-4177-9c60-3b0f8bd2fdbc","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.322Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"7112ab05","correlationId":"7a30aae2-0dd7-4177-9c60-3b0f8bd2fdbc","count":1,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.356Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"43137a7d","correlationId":"7867aec6-e503-462b-a005-7d5a3d0cf7c0","limit":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.361Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"43137a7d","correlationId":"7867aec6-e503-462b-a005-7d5a3d0cf7c0","count":0,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.372Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"a +d724af8","correlationId":"c90ad972-f384-4f77-9010-881faab28463","limit":50,"since":"2024-01-01T00:00:00.000Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.377Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"ad724af8","correlationId":"c90ad972-f384-4f77-9010-881faab28463","count":0,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.415Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"fb6bbaeb","correlationId":"2f63d2ce-f105-4e66-8cfb-b0eca5c2293f","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.420Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"fb6bbaeb","correlationId":"2f63d2ce-f105-4e66-8cfb-b0eca5c2293f","count":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-stats-cache.test.ts +PASS src/services/idempotency-key-service.test.ts +PASS src/utils/payload-compression.test.ts + ● Console + + console.log + 2026-10-16T23:43:28.068Z info: Payload compressed +{"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.116Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.122Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-health-monitor.test.ts + ● Console + + console.log + 2026-10-16T23:43:28.260Z info: NotificationHealthMonitor started {"intervalMs":30000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.294Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-policy.test.ts +PASS src/api/rate-limiter.test.ts +PASS src/migrations/005-notification-expiration.test.ts + ● Console + + console.log + 2026-10-16T23:43:28.057Z info: Migration 005 (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.240Z info: Migration 005-repeat (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.322Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.412Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/rpc-rate-limiter.test.ts + ● Console + + console.log + 2026-10-16T23:43:27.014Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.096Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.164Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.224Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.279Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.450Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.467Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.489Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.504Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.529Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.547Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.569Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.593Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.622Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.643Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.678Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.744Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.775Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.812Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.841Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.864Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.906Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.940Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.985Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.021Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.046Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.082Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.101Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.128Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.160Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.177Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.201Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.228Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.255Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.271Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.300Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.335Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.359Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.389Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.416Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.448Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-scheduler.test.ts + ● Console + + console.log + 2026-10-16T23:43:28.814Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.847Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.852Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.854Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.866Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.870Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.873Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.876Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.888Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.896Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.907Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.914Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.928Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/app-version.test.ts +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts + ● Console + + console.log + 2026-10-16T23:43:27.292Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.326Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.328Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.330Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.672Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.675Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.678Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.693Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.517Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.522Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.535Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.538Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.960Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.969Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.975Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.985Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/logger.test.ts +PASS src/schema/compatibility-check.test.ts +PASS src/services/event-processing-queue.test.ts +PASS src/services/event-store-consistency.test.ts + ● Console + + console.log + 2026-10-16T23:43:29.527Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.555Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.558Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.565Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.606Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/database-recovery.test.ts + ● Console + + console.log + 2026-10-16T23:43:25.942Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.645Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.649Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.656Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.680Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('after')","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.683Z warn: Database connection failure detected; attempting recovery +{"path":"./data/test-db-recovery.db","attempt":1,"maxReconnectAttempts":3,"delayMs":10,"error":"SQLITE_MISUSE: +Database is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.718Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.722Z warn: Database connection recovered {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.771Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:26.782Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.603Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.605Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.611Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.621Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.624Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"maxReconnectAttempts":2,"delayMs":1,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.659Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.662Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.666Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.668Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"maxReconnectAttempts":2,"delayMs":2,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.690Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.692Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.717Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:27.773Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.420Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.422Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.424Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.468Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('dup')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.472Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.483Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.118Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.119Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.121Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.158Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('kept')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.168Z error: Transaction rolled back +{"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.175Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.182Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.773Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.777Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.778Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.785Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/redact.test.ts +PASS src/services/event-deduplication-service.test.ts +PASS src/test-utils/notification-fixture-builder.test.ts +PASS src/services/event-subscriber-checkpoint.test.ts +PASS src/store/preference-store.test.ts +PASS src/api/notifications-history.test.ts +PASS src/services/dns-rebinding-guard.test.ts +PASS src/services/worker-manager.test.ts + ● Console + + console.log + 2026-10-16T23:43:28.483Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.519Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.523Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.549Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.559Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.561Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.573Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.579Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.587Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.591Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:28.598Z info: Initiating graceful shutdown {"activeJobs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.611Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.614Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.621Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.637Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:29.644Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.646Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.648Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.651Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.653Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.656Z warn: Graceful shutdown already initiated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.659Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:43:30.664Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:30.669Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/store/event-registry.test.ts +PASS src/services/providers/provider-health.test.ts + ● Console + + console.log + 2026-10-16T23:43:30.896Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/providers/provider-capabilities.test.ts +PASS src/__tests__/load.test.ts + ● Console + + console.log + + === Load Test Results (100 events) === + + at Object. (src/__tests__/load.test.ts:62:15) + + console.log + Total processing time: 230ms + + at Object. (src/__tests__/load.test.ts:63:15) + + console.log + Throughput: 434.78 events/second + + at Object. (src/__tests__/load.test.ts:64:15) + + console.log + Average latency per event: 2.30ms + + at Object. (src/__tests__/load.test.ts:65:15) + + console.log + + === Load Test Results (500 events, maxConcurrency=20) === + + at Object. (src/__tests__/load.test.ts:109:15) + + console.log + Total processing time: 540ms + + at Object. (src/__tests__/load.test.ts:110:15) + + console.log + Throughput: 925.93 events/second + + at Object. (src/__tests__/load.test.ts:111:15) + + console.log + Average latency per event: 1.08ms + + at Object. (src/__tests__/load.test.ts:112:15) + +PASS src/api/templates-api.test.ts +PASS src/services/provider-registry.test.ts + ● Console + + console.log + 2026-10-16T23:43:31.422Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock +Provider","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.454Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing +Mock","capabilities":[]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.467Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial +Mock","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-api.test.ts + ● Console + + console.log + 2026-10-16T23:43:31.384Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.443Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.447Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.466Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.472Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.501Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-analytics-aggregator.test.ts +PASS src/__tests__/api-contract.test.ts + ● Console + + console.log + 2026-10-16T23:43:31.478Z info: Handling GET /api/events +{"requestId":"93e4275d","correlationId":"ed0679df-9a5e-4b69-8414-715723d32609","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.491Z info: GET /api/events complete +{"requestId":"93e4275d","correlationId":"ed0679df-9a5e-4b69-8414-715723d32609","returned":0,"durationMs":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.526Z warn: Unhandled request {"requestId":"49af7c68","correlationId":"a6a9e419-a4e1 +-44ff-98e5-7536a8cccbc1","method":"GET","url":"/api/definitely-not-a-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.532Z info: Request completed +{"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":6,"requestId":"49af7c68"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.545Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.559Z info: Handling GET /api/analytics {"requestId":"111a04e0","correlationId":"902 +56f8e-21ea-4a7d-8b76-a1df0357aac4","totalRecorded":0,"reset":false,"durationMs":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.569Z info: Handling GET /api/events +{"requestId":"41da4d70","correlationId":"14e596d8-ef25-4b2d-937b-57f3474a997d","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.572Z info: GET /api/events complete +{"requestId":"41da4d70","correlationId":"14e596d8-ef25-4b2d-937b-57f3474a997d","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load-test-runner.test.ts +PASS src/api/content-type.test.ts +PASS src/utils/batch-validator.test.ts +PASS src/api/rate-limit-scenarios.test.ts +PASS src/services/notification-metrics-runner.test.ts +PASS src/utils/metadata-validator.test.ts +PASS src/services/database-cleanup-job.test.ts + ● Console + + console.log + 2026-10-16T23:43:30.065Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.848Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.978Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.063Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/scripts/replay-event.test.ts + ● Console + + console.log + 2026-10-16T23:43:31.807Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.830Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.836Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.845Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.854Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.859Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.874Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.879Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.904Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.921Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.939Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.956Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.958Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.960Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.967Z info: Event processed and recorded {"eventId":"duplicate-test-event","contract +Address":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a0 +26ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.976Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.988Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.008Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.011Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.014Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.024Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAd +dress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de +1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.027Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.035Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.051Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.054Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.060Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.067Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.072Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.084Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.086Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.087Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.091Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.099Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.117Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.119Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.121Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.135Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/scheduled-notification-repository-cache.test.ts + ● Console + + console.log + 2026-10-16T23:43:31.835Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.856Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.859Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.865Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.905Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.942Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.961Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:31.998Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.006Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.019Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.023Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.028Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.035Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.041Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.044Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.052Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.059Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + Performance comparison: + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:130:15) + + console.log + Uncached call: 7ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:131:15) + + console.log + Cached call: 1ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:132:15) + + console.log + Improvement: 85.7% + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:133:15) + + console.log + 2026-10-16T23:43:32.078Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.086Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.092Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.098Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.101Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.117Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.119Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.122Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.161Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/delivery-receipt-repository.test.ts +PASS src/utils/json-logger-formatter.test.ts +PASS src/utils/event-utils.test.ts +PASS src/services/historical-backfill-service.test.ts +PASS src/services/subscription-service.test.ts +PASS src/utils/payload-size-validator.test.ts +PASS src/services/retry-statistics.test.ts +PASS src/api/request-size-limiter.test.ts +PASS src/utils/pagination.test.ts +PASS src/api/cors-validator.test.ts +PASS src/utils/ledger-range-validator.test.ts +PASS src/__tests__/integration.test.ts + ● Console + + console.log + 2026-10-16T23:43:33.085Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":2,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/backpressure-controller.test.ts + ● Console + + console.log + 2026-10-16T23:43:33.139Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.202Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.205Z info: Backpressure deactivated: queue recovered +{"queueSize":49,"threshold":50,"durationMs":3,"targetThroughputPerSec":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.208Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.224Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.261Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.281Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.315Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.331Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-service.test.ts + ● Console + + console.log + 2026-10-16T23:43:32.768Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.790Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.793Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.806Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.811Z info: Notification template created {"templateId":"svc-template"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.833Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.896Z info: Template audit record created +{"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.906Z info: [TemplateCache] Template invalidated +{"templateId":"svc-template","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.920Z info: Notification template updated +{"templateId":"svc-template","actor":"service-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.944Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.959Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.976Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.992Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.994Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.996Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.002Z info: Notification template created {"templateId":"tmpl-a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.005Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.009Z info: Notification template created {"templateId":"tmpl-b"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.017Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.022Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.036Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.061Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.067Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.070Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.091Z info: Notification template created {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.093Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.106Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.108Z info: Notification template deleted {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.120Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.144Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.153Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.186Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.194Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.240Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.328Z info: Notification template created {"templateId":"render-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.330Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.341Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.350Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.372Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.374Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.376Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.381Z info: Notification template created {"templateId":"render-missing"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.383Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.390Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.395Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.411Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.414Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.415Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.421Z info: Notification template created {"templateId":"no-vars"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.423Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.428Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (99.1 s) + ● Notification delivery lifecycle (e2e) › happy path delivery › delivers a contract event to Discord and marks +fingerprint as sent + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   93 | // ========================================================================= +  94 | describe('happy path delivery', () => { + > 95 | it('delivers a contract event to Discord and marks fingerprint as +sent', async () => { +  | ^ +  96 | const service = new +DiscordNotificationService(discordConfig); +  97 | const result = await +service.sendEventNotification(makeEvent(), contractCfg, 'req-happy'); +  98 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:95:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › includes ledger number in the embed + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   114 | }); +  115 | + > 116 | it('includes ledger number in the embed', +async () => { +  | ^ +  117 | const service = new +DiscordNotificationService(discordConfig); +  118 | await service.sendEventNotification( +  119 | makeEvent({ txHash: 'tx-abc123', ledger: +42000 }), + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:116:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › returns true and does not call fetch for duplicate +events (deduplication) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   131 | }); +  132 | + > 133 | it('returns true and does not call fetch for duplicate events +(deduplication)', async () => { +  | ^ +  134 | const service = new +DiscordNotificationService(discordConfig); +  135 | const event = makeEvent({ id: 'evt-dedup-1' +}); +  136 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:133:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders contract events with color 0xff00 + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders system events with color 0x99ff + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles multi-symbol topic arrays + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   253 | // ========================================================================= +  254 | describe('event topic rendering', () => { + > 255 | it('handles multi-symbol topic arrays', +async () => { +  | ^ +  256 | const service = new +DiscordNotificationService(discordConfig); +  257 | const event = makeEvent({ +  258 | topic: +[xdr.ScVal.scvSymbol('task'), +xdr.ScVal.scvSymbol('completed')], + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:255:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:254:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   284 | // ========================================================================= +  285 | describe('network errors', () => { + > 286 | it('returns false and logs error when fetch +throws', async () => { +  | ^ +  287 | fetchMock.mockRejectedValueOnce(new +Error('ECONNRESET')); +  288 | +  289 | const service = new +DiscordNotificationService(discordConfig); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:286:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   304 | }); +  305 | + > 306 | it('returns false on timeout (abort)', +async () => { +  | ^ +  307 | // Simulate a slow response that gets aborted +  308 | fetchMock.mockImplementationOnce( +  309 | () => + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:306:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › allows same event after dedup window expires + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   330 | // ========================================================================= +  331 | describe('deduplication', () => { + > 332 | it('allows same event after dedup window +expires', async () => { +  | ^ +  333 | const dedup = new NotificationDeduplicator({ +  334 | windowMs: 100, +  335 | maxSize: 100, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:332:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › evicts oldest entry when cache is at capacity + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   355 | }); +  356 | + > 357 | it('evicts oldest entry when cache is at +capacity', async () => { +  | ^ +  358 | const dedup = new NotificationDeduplicator({ +maxSize: 2, windowMs: 60000 }); +  359 | const service = new +DiscordNotificationService(discordConfig, dedup); +  360 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:357:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › delivery report › DiscordNotificationService tracks metrics via +deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   409 | // ========================================================================= +  410 | describe('delivery report', () => { + > 411 | it('DiscordNotificationService tracks metrics via +deduplicator', async () => { +  | ^ +  412 | const service = new +DiscordNotificationService(discordConfig); +  413 | +  414 | await service.sendEventNotification(makeEvent({ id: +'evt-metrics-1' }), contractCfg); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:411:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:410:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +PASS src/services/notification-import-service.test.ts + ● Console + + console.log + 2026-10-16T23:43:34.058Z info: Bulk notification import complete +{"imported":2,"skipped":0,"total":2,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.080Z info: Bulk notification import complete +{"imported":1,"skipped":2,"total":3,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.085Z info: Bulk notification import complete +{"imported":1,"skipped":1,"total":2,"format":"csv"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/tests/events-server.test.ts + ● Console + + console.log + 2026-10-16T23:43:33.146Z info: Handling GET /api/events +{"requestId":"a7468fa2","correlationId":"fe242518-1e25-4ff0-ba65-c7982ab60420","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.159Z info: GET /api/events complete +{"requestId":"a7468fa2","correlationId":"fe242518-1e25-4ff0-ba65-c7982ab60420","returned":0,"durationMs":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.191Z info: Handling GET /api/events +{"requestId":"d41256fa","correlationId":"my-trace-abc123","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.204Z info: GET /api/events complete +{"requestId":"d41256fa","correlationId":"my-trace-abc123","returned":0,"durationMs":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.244Z info: Handling GET /api/events +{"requestId":"ff59b050","correlationId":"1b8c07dc-b9cf-42d1-aae4-e326468cf0c7","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.263Z info: GET /api/events complete +{"requestId":"ff59b050","correlationId":"1b8c07dc-b9cf-42d1-aae4-e326468cf0c7","returned":0,"durationMs":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.280Z warn: Unhandled request +{"requestId":"b1a5902a","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.307Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":14,"requestId":"b1a5902a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.367Z warn: Unhandled request +{"requestId":"78620be6","correlationId":"c80917c2-8353-4829-a7be-a9a8fd2ff606","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.370Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":3,"requestId":"78620be6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.398Z warn: Unhandled request +{"requestId":"e4db9b14","correlationId":"35d9d082-539a-4338-8e33-85ce05ed3263","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.402Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":5,"requestId":"e4db9b14"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.425Z info: Handling GET /api/events +{"requestId":"2f242a38","correlationId":"f60b4684-6d0f-45ee-9669-22fbe0c5831e","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.427Z info: GET /api/events complete +{"requestId":"2f242a38","correlationId":"f60b4684-6d0f-45ee-9669-22fbe0c5831e","returned":20,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.436Z info: Handling GET /api/events +{"requestId":"86551c33","correlationId":"0a9b7975-62b9-42d7-a537-d3531e17db29","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.444Z info: GET /api/events complete +{"requestId":"86551c33","correlationId":"0a9b7975-62b9-42d7-a537-d3531e17db29","returned":50,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.452Z info: Handling GET /api/events +{"requestId":"1fe2e72f","correlationId":"780ab20d-fe0e-4cb7-aa80-473a5671fe27","limit":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.455Z info: GET /api/events complete +{"requestId":"1fe2e72f","correlationId":"780ab20d-fe0e-4cb7-aa80-473a5671fe27","returned":100,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.477Z info: Handling GET /api/events +{"requestId":"279f6b8e","correlationId":"00c1d110-681f-4734-baa1-52686afdcd17","limit":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.482Z info: GET /api/events complete +{"requestId":"279f6b8e","correlationId":"00c1d110-681f-4734-baa1-52686afdcd17","returned":1,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.490Z info: Handling GET /api/events +{"requestId":"a7f672a5","correlationId":"007158fd-41ca-40e0-bef4-fbff326d423f","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.494Z info: GET /api/events complete +{"requestId":"a7f672a5","correlationId":"007158fd-41ca-40e0-bef4-fbff326d423f","returned":20,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/search-suggestion.test.ts + ● Console + + console.log + 2026-10-16T23:43:33.679Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.701Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.708Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.717Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.790Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/shutdown-unit.test.ts +PASS src/services/notification-template-repository.test.ts + ● Console + + console.log + 2026-10-16T23:43:33.253Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.344Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.346Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.353Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.357Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.378Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.382Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.399Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.400Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.402Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.418Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.421Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.435Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.437Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.438Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.442Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.450Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.480Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.482Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.487Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.491Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.502Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.513Z info: Notification template updated +{"templateId":"tmpl-001","actor":"admin@example.com"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.523Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.527Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.538Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.543Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.545Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.549Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.553Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.556Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.561Z info: Template audit record created +{"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.564Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.568Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.572Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.584Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.586Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.587Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.590Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.603Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.608Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.619Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.620Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.622Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.624Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.627Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.630Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.644Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.645Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.646Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.648Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.651Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.654Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.665Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.668Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.670Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.673Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.675Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.682Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.685Z info: [TemplateCache] Template invalidated +{"templateId":"tmpl-001","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.686Z info: Notification template updated +{"templateId":"tmpl-001","actor":"cache-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.689Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.692Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.704Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.708Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.711Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.719Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.734Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.742Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.746Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET +actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.750Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.753Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.769Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.770Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.772Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.774Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.777Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.779Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.783Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log +WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.786Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.789Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.799Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.801Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.802Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.825Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.827Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.839Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.840Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.841Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.845Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/tests/notification-scheduler-refactored.test.ts + ● Console + + console.log + 2026-10-16T23:43:32.035Z info: Connected to SQLite database +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.658Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.661Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.673Z info: Database initialized successfully +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.691Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.779Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.802Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.820Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.902Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:32.951Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.039Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.063Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.155Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.324Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.360Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.408Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.429Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.477Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.520Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.523Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.556Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.574Z info: Notification cancelled {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.614Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.628Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.676Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.691Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.731Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.779Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.795Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.829Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.868Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.886Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.902Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.925Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.945Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.962Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.985Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:33.999Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.017Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.028Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.033Z info: Scheduling new notification +{"type":"email","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.045Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.050Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.064Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.068Z info: Scheduling new notification +{"type":"sms","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.080Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"sms"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.107Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.121Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.129Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/dead-letter-queue.test.ts + ● Console + + console.log + 2026-10-16T23:43:33.344Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.013Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.016Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.021Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.035Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.067Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.069Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T23:43:39.044Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.076Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.082Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.652Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.654Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.655Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.669Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.692Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"email","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.694Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T23:43:39.671Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.707Z info: Dead-letter notification requeued +{"requestId":"req-2","id":1,"notificationId":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.713Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/job-monitor.test.ts + ● Console + + console.log + 2026-10-16T23:43:34.407Z error: Job monitor: job failed +{"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/migrations/004-database-data-integrity.test.ts + ● Console + + console.log + 2026-10-16T23:43:34.745Z info: Migration 004 (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.851Z info: Migration 004-repeat (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.900Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 +aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 +row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data +first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.920Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.942Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.945Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.947Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.952Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', +'2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.954Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/dead-letter-manager.test.ts +PASS src/config-logging.test.ts +PASS src/services/notification-search-service.test.ts + ● Console + + console.log + 2026-10-16T23:43:34.596Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.624Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.629Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.636Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:34.642Z info: Notification search complete +{"total":1,"returned":1,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/request-id.test.ts +PASS src/services/webhook-replay-cache.test.ts +PASS src/examples/webhook-consumer-verification-example.test.ts +PASS src/services/event-quarantine.test.ts +PASS src/api/api-version-registry.test.ts +PASS src/utils/logger-structured.test.ts +PASS src/services/event-provenance.test.ts + ● Console + + console.log + 2026-10-16T23:43:35.779Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.892Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.894Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.904Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.983Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-normalization.test.ts +PASS src/services/subscription-event-emitter.test.ts +PASS src/middleware/body-limit.test.ts +PASS src/middleware/security-headers.test.ts +PASS src/middleware/request-id.test.ts +PASS src/utils/notification-version.test.ts +PASS src/utils/config-snapshot.test.ts +PASS src/services/template-service.test.ts + ● Console + + console.log + 2026-10-16T23:43:36.416Z info: Template created successfully +{"templateId":1,"uniqueKey":"welcome-email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.459Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome +{{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.466Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" +","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.480Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.490Z error: Failed to create template {"error":{"message":"description: must be a +string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","description":12345}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.499Z error: Failed to create template {"error":{"message":"name: must not exceed +255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTe +mplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.506Z error: Failed to create template {"error":{"message":"variables: must be an +array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","variables":[1,2,3]}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.512Z error: Failed to create template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","defaultValues":"nope"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.519Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.528Z error: Failed to update template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.536Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:36.548Z error: Failed to update template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/log-level-resolver.test.ts +PASS src/config/schema-validator.test.ts +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/services/notification-metrics-store.test.ts +PASS src/api/events-server.health.test.ts +PASS src/utils/cors-validator.test.ts +PASS src/services/notification-template-cache.test.ts + ● Console + + console.log + 2026-10-16T23:43:37.176Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.209Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.213Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.221Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.225Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.228Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.235Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.238Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.241Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.244Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.253Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.256Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.259Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.263Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.267Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.269Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.272Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.274Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.276Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.279Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.284Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.289Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.292Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.296Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.304Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.308Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.311Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.317Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/events-server.history.test.ts +PASS src/utils/circuit-breaker.test.ts +PASS src/services/circuit-breaker.test.ts + ● Console + + console.log + 2026-10-16T23:43:35.771Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.801Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.805Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.807Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.809Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.813Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.827Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.833Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.837Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194216837} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.842Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.857Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.860Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.871Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194216870} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.873Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.875Z warn: Circuit breaker is OPEN, blocking request +{"state":"open","nextAttemptTime":1792194216870} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.880Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.882Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.888Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194216887} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:35.891Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.003Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.011Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.015Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.024Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194218023} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:37.035Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.141Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.143Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.145Z info: Circuit breaker closed after successful recovery +{"successCount":0,"successThreshold":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.147Z info: Circuit breaker recorded success +{"state":"closed","successCount":0,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.150Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.151Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.153Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194219153} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:38.155Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.259Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.261Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.263Z error: Circuit breaker opened due to repeated failures +{"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194220263} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.269Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.273Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.276Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.278Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194220278} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.282Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.288Z info: Circuit breaker manually reset {"previousState":"closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.295Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.298Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.301Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.304Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.306Z error: Circuit breaker opened due to repeated failures +{"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792194220306} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.308Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":5,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:43:39.312Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/discord-notification-refactored.test.ts (115.459 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   243 | const result = await +service.sendTestMessage(); +  244 | + > 245 | expect(result).toBe(true); +  | ^ +  246 | }); +  247 | +  248 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification-refactored.test.ts:245:22) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/__tests__/stress.test.ts (230.159 s) + ● Console + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 1,000 Concurrent Events + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 730ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1369.86 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 31.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 12.49ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 12.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 26.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 27.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 170.86 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 176.88 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 6.03 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 250.91 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 220.97 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -29.94 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 5,000 Events Sustained Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 3990ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1253.13 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 139.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 20.60ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 55.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 138.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 179.61 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 200.92 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 21.30 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 225.45 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 26.05 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -199.40 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + Progress: 3500/10000 (35.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 8700/10000 (87.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 10,000 Events - System Stability + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 10738ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 931.27 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 244.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 19.79ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 12.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 82.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 156.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 203.54 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 207.76 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 4.21 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 36.89 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 85.77 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 48.88 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + Sending first burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:356:15) + + console.log + Waiting idle period (2 seconds)... + + at Object. (src/__tests__/stress.test.ts:373:15) + + console.log + Sending second burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:377:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Burst Traffic (2x2000 events) + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 4040ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 990.10 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 39.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 12.14ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 11.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 29.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 35.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 210.80 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 234.50 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 23.70 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 91.94 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 92.64 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 0.70 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + First burst time: 1095ms + + at Object. (src/__tests__/stress.test.ts:406:15) + + console.log + Second burst time: 940ms + + at Object. (src/__tests__/stress.test.ts:407:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Priority Queue Under Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 1318ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1517.45 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 43.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 12.34ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 12.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 22.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 41.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 236.47 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 203.70 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: -32.78 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 94.90 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 88.87 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -6.03 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Priority Analysis: + + at Object. (src/__tests__/stress.test.ts:492:15) + + console.log + High-priority in first 500: 500 / 500 (100.00%) + + at Object. (src/__tests__/stress.test.ts:493:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Deduplication Test (3000 events, 1500 unique) + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 3000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 1500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 1799ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1667.59 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 3.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 26.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 13.55ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 14.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 26.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 26.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 205.31 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 214.35 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 9.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 90.82 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 99.86 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 9.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Deduplication Efficiency: + + at Object. (src/__tests__/stress.test.ts:563:15) + + console.log + Total Enqueued: 3000 + + at Object. (src/__tests__/stress.test.ts:564:15) + + console.log + Unique Events: 1500 + + at Object. (src/__tests__/stress.test.ts:565:15) + + console.log + Actually Processed: 1500 + + at Object. (src/__tests__/stress.test.ts:566:15) + + console.log + Duplicates Blocked: 1500 + + at Object. (src/__tests__/stress.test.ts:567:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Discord Notification Service Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 460ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1086.96 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 162.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 454.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 250.13ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 223.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 384.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 391.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 215.96 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 232.01 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 16.06 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 103.45 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 120.36 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 16.91 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Discord Service Metrics: + + at Object. (src/__tests__/stress.test.ts:630:15) + + console.log + Total Sent: undefined + + at Object. (src/__tests__/stress.test.ts:631:15) + + console.log + Duplicates Skipped: undefined + + at Object. (src/__tests__/stress.test.ts:632:15) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + ================================================================================". + +   94 | +  95 | // Log to console for visibility + > 96 | console.log('\n' + +'='.repeat(80)); +  | ^ +  97 | console.log(`STRESS TEST REPORT: ${testName}`); +  98 | console.log('='.repeat(80)); +  99 | console.log(`Events Processed: ${eventCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "STRESS TEST REPORT: Error Recovery Under Load". + +   95 | // Log to console for visibility +  96 | console.log('\n' + +'='.repeat(80)); + > 97 | console.log(`STRESS TEST REPORT: +${testName}`); +  | ^ +  98 | console.log('='.repeat(80)); +  99 | console.log(`Events Processed: ${eventCount}`); +  100 | console.log(`Success Count: ${successCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================". + +   96 | console.log('\n' + +'='.repeat(80)); +  97 | console.log(`STRESS TEST REPORT: ${testName}`); + > 98 | +console.log('='.repeat(80)); +  | ^ +  99 | console.log(`Events Processed: ${eventCount}`); +  100 | console.log(`Success Count: ${successCount}`); +  101 | console.log(`Failure Count: ${failureCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Events Processed: 1000". + +   97 | console.log(`STRESS TEST REPORT: ${testName}`); +  98 | console.log('='.repeat(80)); + > 99 | console.log(`Events Processed: +${eventCount}`); +  | ^ +  100 | console.log(`Success Count: ${successCount}`); +  101 | console.log(`Failure Count: ${failureCount}`); +  102 | console.log(`Total Time: ${totalTimeMs}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Success Count: 900". + +   98 | console.log('='.repeat(80)); +  99 | console.log(`Events Processed: ${eventCount}`); + > 100 | console.log(`Success Count: +${successCount}`); +  | ^ +  101 | console.log(`Failure Count: ${failureCount}`); +  102 | console.log(`Total Time: ${totalTimeMs}ms`); +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Failure Count: 0". + +   99 | console.log(`Events Processed: ${eventCount}`); +  100 | console.log(`Success Count: ${successCount}`); + > 101 | console.log(`Failure Count: +${failureCount}`); +  | ^ +  102 | console.log(`Total Time: ${totalTimeMs}ms`); +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  104 | console.log(`\nLatency Statistics:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Total Time: 180349ms". + +   100 | console.log(`Success Count: ${successCount}`); +  101 | console.log(`Failure Count: ${failureCount}`); + > 102 | console.log(`Total Time: +${totalTimeMs}ms`); +  | ^ +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  104 | console.log(`\nLatency Statistics:`); +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Throughput: 5.54 events/second". + +   101 | console.log(`Failure Count: ${failureCount}`); +  102 | console.log(`Total Time: ${totalTimeMs}ms`); + > 103 | console.log(`Throughput: +${throughput.toFixed(2)} events/second`); +  | ^ +  104 | console.log(`\nLatency Statistics:`); +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Latency Statistics:". + +   102 | console.log(`Total Time: ${totalTimeMs}ms`); +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + > 104 | console.log(`\nLatency Statistics:`); +  | ^ +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Min: 0.00ms". + +   103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  104 | console.log(`\nLatency Statistics:`); + > 105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  | ^ +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Max: 36.00ms". + +   104 | console.log(`\nLatency Statistics:`); +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + > 106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  | ^ +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Avg: 11.59ms". + +   105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + > 107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  | ^ +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); +  110 | console.log(` P99: ${p99.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P50 (Median): 11.00ms". + +   106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + > 108 | console.log(` P50 (Median): +${p50.toFixed(2)}ms`); +  | ^ +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); +  110 | console.log(` P99: ${p99.toFixed(2)}ms`); +  111 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P95: 24.00ms". + +   107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + > 109 | console.log(` P95: +${p95.toFixed(2)}ms`); +  | ^ +  110 | console.log(` P99: ${p99.toFixed(2)}ms`); +  111 | +  112 | if (memoryUsage) { + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P99: 26.00ms". + +   108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); + > 110 | console.log(` P99: +${p99.toFixed(2)}ms`); +  | ^ +  111 | +  112 | if (memoryUsage) { +  113 | console.log(`\nMemory Usage:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Memory Usage:". + +   111 | +  112 | if (memoryUsage) { + > 113 | console.log(`\nMemory Usage:`); +  | ^ +  114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - Heap: 233.42 MB". + +   112 | if (memoryUsage) { +  113 | console.log(`\nMemory Usage:`); + > 114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  | ^ +  115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - Heap: 181.97 MB". + +   113 | console.log(`\nMemory Usage:`); +  114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); + > 115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  | ^ +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - Heap: -51.45 MB". + +   114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); + > 116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  | ^ +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - RSS: 121.84 MB". + +   115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + > 117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  | ^ +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  120 | } + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - RSS: 31.61 MB". + +   116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + > 118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  | ^ +  119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  120 | } +  121 | console.log('='.repeat(80) + +'\n'); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - RSS: -90.23 MB". + +   117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + > 119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  | ^ +  120 | } +  121 | console.log('='.repeat(80) + +'\n'); +  122 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================ + ". + +   119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  120 | } + > 121 | console.log('='.repeat(80) ++ '\n'); +  | ^ +  122 | +  123 | return report; +  124 | }; + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Error Recovery Statistics:". + +   707 | ); +  708 | + > 709 | console.log(`\nError Recovery +Statistics:`); +  | ^ +  710 | console.log(` Initial Failures: ${retryCount}`); +  711 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  712 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:709:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Initial Failures: 300". + +   708 | +  709 | console.log(`\nError Recovery Statistics:`); + > 710 | console.log(` Initial Failures: +${retryCount}`); +  | ^ +  711 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  712 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  713 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:710:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Successful Retries: 200". + +   709 | console.log(`\nError Recovery Statistics:`); +  710 | console.log(` Initial Failures: ${retryCount}`); + > 711 | console.log(` Successful Retries: ${processedIds.length +- (eventCount - retryCount)}`); +  | ^ +  712 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  713 | +  714 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:711:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Final Success Rate: 90.00%". + +   710 | console.log(` Initial Failures: ${retryCount}`); +  711 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); + > 712 | console.log(` Final Success Rate: +${((processedIds.length / eventCount) * 100).toFixed(2)}%`); +  | ^ +  713 | +  714 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success +  715 | expect(retryCount).toBeGreaterThan(0); // Verify +retries occurred + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:712:15) + +A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due +to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure +that .unref() was called on them. +Summary of all failing tests +FAIL src/services/scheduled-notification-queue.test.ts (52.181 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/services/concurrent-event-processing.test.ts (55.885 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/retry-deduplication.test.ts (63.88 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (64.41 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/services/execution-metrics.test.ts (11.86 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/api/queue-operational-metrics.test.ts (17.115 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +FAIL src/tests/payload-integrity.test.ts + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/services/discord-notification.test.ts (18.095 s) + ● DiscordNotificationService › sendEventNotification › should send event notification successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   58 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  59 | + > 60 | expect(result).toBe(true); +  | ^ +  61 | expect(mockFetch).toHaveBeenCalledTimes(1); +  62 | const [url, options] = +mockFetch.mock.calls[0]; +  63 | expect(url).toBe(mockConfig.webhookUrl); + + at Object. (src/services/discord-notification.test.ts:60:22) + + ● DiscordNotificationService › sendEventNotification › should handle request timeout + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Discord webhook request timed out", ObjectContaining {"timeoutMs": 100, "webhookId": "123"} + Received + 1: "Discord webhook request error", {"attempt": 0, "contractAddress": "CA123", "correlationId": +"ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", "durationMs": 349, "error": [DnsRebindingError: DNS resolution returned no +addresses for hostname: discord.com], "eventId": "event-123", "requestId": "ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", +"webhookId": "123"} + 2: "Exceeded max Discord retry attempts", {"contractAddress": "CA123", "correlationId": +"ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", "eventId": "event-123", "maxRetries": 0, "requestId": +"ac3d0b2c-39d1-45bc-af5a-9a6860eb8328", "webhookId": "123"} + + Number of calls: 2 + +   111 | +  112 | expect(result).toBe(false); + > 113 | expect(mockLoggerModule.error).toHaveBeenCalledWith( +  | ^ +  114 | 'Discord webhook request timed out', +  115 | expect.objectContaining({ +  116 | webhookId: mockConfig.webhookId, + + at Object. (src/services/discord-notification.test.ts:113:38) + + ● DiscordNotificationService › duplicate detection › skips the webhook call for a duplicate event + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   133 | const secondResult = await +service.sendEventNotification(mockEvent, mockContractConfig); +  134 | + > 135 | +expect(mockFetch).toHaveBeenCalledTimes(1); +  | ^ +  136 | expect(secondResult).toBe(true); +  137 | expect(service.getDeduplicationMetrics()).toEqual( +  138 | expect.objectContaining({ skippedDuplicates: 1, +cacheSize: 1 }), + + at Object. (src/services/discord-notification.test.ts:135:25) + + ● DiscordNotificationService › duplicate detection › logs a duplicate detection event + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping duplicate notification", ObjectContaining {"contractAddress": "CA123", "eventId": +"event-dup-log"} + Received + 1: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"07b1bfbc-02de-4047-953b-a0b67cc8bdae", "eventId": "event-dup-log", "requestId": +"07b1bfbc-02de-4047-953b-a0b67cc8bdae", "webhookId": "123"} + 2: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"bd2c1bf6-fb61-4f33-a8e8-57dc5866f05d", "eventId": "event-dup-log", "requestId": +"bd2c1bf6-fb61-4f33-a8e8-57dc5866f05d", "webhookId": "123"} + + Number of calls: 2 + +   151 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  152 | + > 153 | expect(mockLoggerModule.info).toHaveBeenCalledWith( +  | ^ +  154 | 'Skipping duplicate notification', +  155 | expect.objectContaining({ +  156 | eventId: 'event-dup-log', + + at Object. (src/services/discord-notification.test.ts:153:37) + + ● DiscordNotificationService › duplicate detection › allows the same notification request after the configured +window expires + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   173 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  174 | + > 175 | expect(result).toBe(true); +  | ^ +  176 | expect(mockFetch).toHaveBeenCalledTimes(2); +  177 | expect(service.getDeduplicationMetrics()).toEqual( +  178 | expect.objectContaining({ acceptedRequests: 2, +skippedDuplicates: 1 }), + + at Object. (src/services/discord-notification.test.ts:175:22) + + ● DiscordNotificationService › duplicate detection › allows the same event id on a different contract through + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 0 + +   191 | const result = await +service.sendEventNotification(mockEvent, contractB); +  192 | + > 193 | +expect(mockFetch).toHaveBeenCalledTimes(2); +  | ^ +  194 | expect(result).toBe(true); +  195 | }); +  196 | + + at Object. (src/services/discord-notification.test.ts:193:25) + + ● DiscordNotificationService › duplicate detection › does not mark an event as sent when the webhook call fails + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   213 | +  214 | expect(firstResult).toBe(false); + > 215 | expect(secondResult).toBe(true); +  | ^ +  216 | expect(mockFetch).toHaveBeenCalledTimes(2); +  217 | }); +  218 | + + at Object. (src/services/discord-notification.test.ts:215:28) + + ● DiscordNotificationService › duplicate detection › accepts an injected deduplicator + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   227 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  228 | + > 229 | +expect(deduplicator.size()).toBe(1); +  | ^ +  230 | +expect(deduplicator.isDuplicate(generateFingerprint('event-injected', +'CA123'))).toBe(true); +  231 | }); +  232 | }); + + at Object. (src/services/discord-notification.test.ts:229:35) + + ● DiscordNotificationService › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   239 | const result = await +service.sendTestMessage(); +  240 | + > 241 | expect(result).toBe(true); +  | ^ +  242 | }); +  243 | +  244 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification.test.ts:241:22) + + ● DiscordNotificationService › formatEventMessage › should format event with string value correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   264 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  265 | + > 266 | expect(result).toBe(true); +  | ^ +  267 | const [, options] = +mockFetch.mock.calls[0]; +  268 | const body = +JSON.parse(options.body); +  269 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:266:22) + + ● DiscordNotificationService › formatEventMessage › should truncate long string values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   283 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  284 | + > 285 | expect(result).toBe(true); +  | ^ +  286 | const [, options] = +mockFetch.mock.calls[0]; +  287 | const body = +JSON.parse(options.body); +  288 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:285:22) + + ● DiscordNotificationService › formatEventMessage › should handle symbol type values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   302 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  303 | + > 304 | expect(result).toBe(true); +  | ^ +  305 | const [, options] = +mockFetch.mock.calls[0]; +  306 | const body = +JSON.parse(options.body); +  307 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:304:22) + + ● DiscordNotificationService › formatEventMessage › should preserve short messages without truncation + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   320 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  321 | + > 322 | expect(result).toBe(true); +  | ^ +  323 | const [, options] = +mockFetch.mock.calls[0]; +  324 | const body = +JSON.parse(options.body); +  325 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:322:22) + + ● DiscordNotificationService › formatEventMessage › should truncate oversized embed titles + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   340 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  341 | + > 342 | expect(result).toBe(true); +  | ^ +  343 | const [, options] = +mockFetch.mock.calls[0]; +  344 | const body = +JSON.parse(options.body); +  345 | expect(body.embeds[0].title.length).toBeL +essThanOrEqual(256); + + at Object. (src/services/discord-notification.test.ts:342:22) + + ● DiscordNotificationService › formatEventMessage › should enforce total embed size limit + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   361 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  362 | + > 363 | expect(result).toBe(true); +  | ^ +  364 | const [, options] = +mockFetch.mock.calls[0]; +  365 | const body = +JSON.parse(options.body); +  366 | const embed = body.embeds[0]; + + at Object. (src/services/discord-notification.test.ts:363:22) + + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   380 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  381 | + > 382 | expect(result).toBe(true); +  | ^ +  383 | const [, options] = +mockFetch.mock.calls[0]; +  384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:382:22) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone +embedded in an scvString payload + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   459 | +  460 | describe('integration: mention in event string value reaches embed +sanitized', () => { + > 461 | it('strips @everyone embedded in an scvString +payload', async () => { +  | ^ +  462 | mockFetch.mockResolvedValueOnce({ ok: true }); +  463 | +  464 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:461:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone in +event topic symbol used as embed title + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   487 | }); +  488 | + > 489 | it('strips @everyone in event topic symbol used as embed +title', async () => { +  | ^ +  490 | mockFetch.mockResolvedValueOnce({ ok: true }); +  491 | +  492 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:489:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 0, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › tracks timeoutCount in DiscordNotificationService on +AbortError + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 1 + Received: 0 + +   71 | const success = await +service.sendTestMessage(); +  72 | expect(success).toBe(false); + > 73 | +expect(service.getMetrics().timeoutCount).toBeGreaterThanOrEqual(1); +  | ^ +  74 | }); +  75 | }); +  76 | + + at Object. (src/services/notification-failure-scenarios.test.ts:73:49) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/services/retry-scheduler-webhook.test.ts + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792107798043} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792194199194, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792107798043} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792194199195, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "ca93f78a"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "2ce0d7c3"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "f832376c-cff6-4b14-bddf-1dc671f56ff0", +"eventId": "event-1", "requestId": "f832376c-cff6-4b14-bddf-1dc671f56ff0"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/tests/api-versioning.test.ts + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (86.693 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   169 | +  170 | describe('fan-out delivery', () => { + > 171 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  172 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  173 | +  174 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:171:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   178 | }); +  179 | + > 180 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  181 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  182 | +  183 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:180:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   194 | +  195 | describe('channel-specific payloads', () => { + > 196 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  197 | await deliverToAllChannels( +  198 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  199 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:196:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   210 | }); +  211 | + > 212 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  213 | await deliverToAllChannels( +  214 | createMockEvent({ id: 'event-system', type: +'system' }) +  215 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:212:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +FAIL src/api/events-server.test.ts + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/webhook-verifier.test.ts + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/api/execution-metrics-api.test.ts (6.544 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/tests/template-api-integration.test.ts + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 5828.474759042641, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:43:29.910Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/api/template-routes.test.ts + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +3849.7143791391845, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:43:29.102Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (99.1 s) + ● Notification delivery lifecycle (e2e) › happy path delivery › delivers a contract event to Discord and marks +fingerprint as sent + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   93 | // ========================================================================= +  94 | describe('happy path delivery', () => { + > 95 | it('delivers a contract event to Discord and marks fingerprint as +sent', async () => { +  | ^ +  96 | const service = new +DiscordNotificationService(discordConfig); +  97 | const result = await +service.sendEventNotification(makeEvent(), contractCfg, 'req-happy'); +  98 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:95:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › includes ledger number in the embed + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   114 | }); +  115 | + > 116 | it('includes ledger number in the embed', +async () => { +  | ^ +  117 | const service = new +DiscordNotificationService(discordConfig); +  118 | await service.sendEventNotification( +  119 | makeEvent({ txHash: 'tx-abc123', ledger: +42000 }), + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:116:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › returns true and does not call fetch for duplicate +events (deduplication) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   131 | }); +  132 | + > 133 | it('returns true and does not call fetch for duplicate events +(deduplication)', async () => { +  | ^ +  134 | const service = new +DiscordNotificationService(discordConfig); +  135 | const event = makeEvent({ id: 'evt-dedup-1' +}); +  136 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:133:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders contract events with color 0xff00 + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders system events with color 0x99ff + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles multi-symbol topic arrays + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   253 | // ========================================================================= +  254 | describe('event topic rendering', () => { + > 255 | it('handles multi-symbol topic arrays', +async () => { +  | ^ +  256 | const service = new +DiscordNotificationService(discordConfig); +  257 | const event = makeEvent({ +  258 | topic: +[xdr.ScVal.scvSymbol('task'), +xdr.ScVal.scvSymbol('completed')], + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:255:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:254:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   284 | // ========================================================================= +  285 | describe('network errors', () => { + > 286 | it('returns false and logs error when fetch +throws', async () => { +  | ^ +  287 | fetchMock.mockRejectedValueOnce(new +Error('ECONNRESET')); +  288 | +  289 | const service = new +DiscordNotificationService(discordConfig); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:286:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   304 | }); +  305 | + > 306 | it('returns false on timeout (abort)', +async () => { +  | ^ +  307 | // Simulate a slow response that gets aborted +  308 | fetchMock.mockImplementationOnce( +  309 | () => + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:306:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › allows same event after dedup window expires + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   330 | // ========================================================================= +  331 | describe('deduplication', () => { + > 332 | it('allows same event after dedup window +expires', async () => { +  | ^ +  333 | const dedup = new NotificationDeduplicator({ +  334 | windowMs: 100, +  335 | maxSize: 100, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:332:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › evicts oldest entry when cache is at capacity + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   355 | }); +  356 | + > 357 | it('evicts oldest entry when cache is at +capacity', async () => { +  | ^ +  358 | const dedup = new NotificationDeduplicator({ +maxSize: 2, windowMs: 60000 }); +  359 | const service = new +DiscordNotificationService(discordConfig, dedup); +  360 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:357:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › delivery report › DiscordNotificationService tracks metrics via +deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   409 | // ========================================================================= +  410 | describe('delivery report', () => { + > 411 | it('DiscordNotificationService tracks metrics via +deduplicator', async () => { +  | ^ +  412 | const service = new +DiscordNotificationService(discordConfig); +  413 | +  414 | await service.sendEventNotification(makeEvent({ id: +'evt-metrics-1' }), contractCfg); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:411:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:410:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/services/discord-notification-refactored.test.ts (115.459 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   243 | const result = await +service.sendTestMessage(); +  244 | + > 245 | expect(result).toBe(true); +  | ^ +  246 | }); +  247 | +  248 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification-refactored.test.ts:245:22) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/__tests__/stress.test.ts (230.159 s) + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + + +Test Suites: 46 failed, 1 skipped, 105 passed, 151 of 152 total +Tests: 282 failed, 4 skipped, 2266 passed, 2552 total +Snapshots: 0 total +Time: 238.645 s, estimated 425 s +Ran all test suites. diff --git a/listener/_jest-fresh.txt b/listener/_jest-fresh.txt new file mode 100644 index 00000000..c0277345 --- /dev/null +++ b/listener/_jest-fresh.txt @@ -0,0 +1,23315 @@ +node : ts-jest[ts-compiler] (WARN) +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:164:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --ci --maxWorkers=4 ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (ts-jest[ts-comp...yQueueOptions'.:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + +164 { baseDelayMs: 1000, maxRetries: 3, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:203:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +203 { baseDelayMs: 1000, maxRetries: 1, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +ts-jest[ts-compiler] (WARN) src/__tests__/stress.test.ts:631:52 - error +TS2339: Property 'totalSent' does not exist on type '{ timeoutCount: number; acceptedRequests: number; +skippedDuplicates: number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: +number; hitRatio: number; totalChecks: number; }'. + +631 console.log(` Total Sent: ${metrics.totalSent}`); +   ~~~~~~~~~ +src/__tests__/stress.test.ts:632:52 - error TS2339: Property +'duplicatesSkipped' does not exist on type '{ timeoutCount: number; acceptedRequests: number; skippedDuplicates: +number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: number; hitRatio: +number; totalChecks: number; }'. + +632 console.log(` Duplicates Skipped: ${metrics.duplicatesSkipped}`); +   ~~~~~~~~~~~~~~~~~ +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (64.658 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   169 | +  170 | describe('fan-out delivery', () => { + > 171 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  172 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  173 | +  174 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:171:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   178 | }); +  179 | + > 180 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  181 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  182 | +  183 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:180:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   194 | +  195 | describe('channel-specific payloads', () => { + > 196 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  197 | await deliverToAllChannels( +  198 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  199 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:196:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   210 | }); +  211 | + > 212 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  213 | await deliverToAllChannels( +  214 | createMockEvent({ id: 'event-system', type: +'system' }) +  215 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:212:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +ts-jest[ts-compiler] (WARN) src/__tests__/blockchain-to-notification-e2e.test.ts:153:34 - +error TS2345: Argument of type '{ events: never[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: never[]; cursor: string; }' is missing the following properties from type 'GetEventsResponse': +latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +153 expect(validateRpcResponse(response).valid).toBe(true); +   ~~~~~~~~ +src/__tests__/blockchain-to-notification-e2e.test.ts:867:42 - error TS2345: +Argument of type '{ events: StellarSDK.rpc.Api.EventResponse[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: EventResponse[]; cursor: string; }' is missing the following properties from type +'GetEventsResponse': latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +867 const rpcCheck = validateRpcResponse(rpcResponse); +   ~~~~~~~~~~~ +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (74.57 s) + ● Notification delivery lifecycle (e2e) › happy path delivery › delivers a contract event to Discord and marks +fingerprint as sent + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   93 | // ========================================================================= +  94 | describe('happy path delivery', () => { + > 95 | it('delivers a contract event to Discord and marks fingerprint as +sent', async () => { +  | ^ +  96 | const service = new +DiscordNotificationService(discordConfig); +  97 | const result = await +service.sendEventNotification(makeEvent(), contractCfg, 'req-happy'); +  98 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:95:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › includes ledger number in the embed + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   114 | }); +  115 | + > 116 | it('includes ledger number in the embed', +async () => { +  | ^ +  117 | const service = new +DiscordNotificationService(discordConfig); +  118 | await service.sendEventNotification( +  119 | makeEvent({ txHash: 'tx-abc123', ledger: +42000 }), + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:116:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › returns true and does not call fetch for duplicate +events (deduplication) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   131 | }); +  132 | + > 133 | it('returns true and does not call fetch for duplicate events +(deduplication)', async () => { +  | ^ +  134 | const service = new +DiscordNotificationService(discordConfig); +  135 | const event = makeEvent({ id: 'evt-dedup-1' +}); +  136 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:133:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders contract events with color 0xff00 + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders system events with color 0x99ff + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles multi-symbol topic arrays + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   253 | // ========================================================================= +  254 | describe('event topic rendering', () => { + > 255 | it('handles multi-symbol topic arrays', +async () => { +  | ^ +  256 | const service = new +DiscordNotificationService(discordConfig); +  257 | const event = makeEvent({ +  258 | topic: +[xdr.ScVal.scvSymbol('task'), +xdr.ScVal.scvSymbol('completed')], + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:255:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:254:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   284 | // ========================================================================= +  285 | describe('network errors', () => { + > 286 | it('returns false and logs error when fetch +throws', async () => { +  | ^ +  287 | fetchMock.mockRejectedValueOnce(new +Error('ECONNRESET')); +  288 | +  289 | const service = new +DiscordNotificationService(discordConfig); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:286:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   304 | }); +  305 | + > 306 | it('returns false on timeout (abort)', +async () => { +  | ^ +  307 | // Simulate a slow response that gets aborted +  308 | fetchMock.mockImplementationOnce( +  309 | () => + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:306:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › allows same event after dedup window expires + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   330 | // ========================================================================= +  331 | describe('deduplication', () => { + > 332 | it('allows same event after dedup window +expires', async () => { +  | ^ +  333 | const dedup = new NotificationDeduplicator({ +  334 | windowMs: 100, +  335 | maxSize: 100, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:332:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › evicts oldest entry when cache is at capacity + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   355 | }); +  356 | + > 357 | it('evicts oldest entry when cache is at +capacity', async () => { +  | ^ +  358 | const dedup = new NotificationDeduplicator({ +maxSize: 2, windowMs: 60000 }); +  359 | const service = new +DiscordNotificationService(discordConfig, dedup); +  360 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:357:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › delivery report › DiscordNotificationService tracks metrics via +deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   409 | // ========================================================================= +  410 | describe('delivery report', () => { + > 411 | it('DiscordNotificationService tracks metrics via +deduplicator', async () => { +  | ^ +  412 | const service = new +DiscordNotificationService(discordConfig); +  413 | +  414 | await service.sendEventNotification(makeEvent({ id: +'evt-metrics-1' }), contractCfg); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:411:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:410:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (19.279 s) + ● Console + + console.log + 2026-10-16T23:55:13.031Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.062Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.066Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.068Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.122Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.124Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.126Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.128Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.321Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.324Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.325Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:13.327Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:18.472Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:18.476Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:18.479Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:18.482Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.268Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.271Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.274Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.275Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.379Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.381Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.385Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.388Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.508Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.511Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.515Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.517Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.565Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.569Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.572Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.575Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +ts-jest[ts-compiler] (WARN) src/services/concurrent-event-processing.test.ts:74:40 - +error TS2352: Conversion of type '{ address: string; name: string; }' to type 'ContractConfig' may +be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the +expression to 'unknown' first. + Property 'events' is missing in type '{ address: string; name: string; }' but required in type 'ContractConfig'. + +74 const contractConfig: ContractConfig = { address: CONTRACT, name: 'Concurrency' } as ContractConfig; +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/types/index.ts:18:3 + 18 events: string[]; +    ~~~~~~ + 'events' is declared here. +FAIL src/services/retry-deduplication.test.ts (14.618 s) + ● Console + + console.log + 2026-10-16T23:55:18.880Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.578Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.581Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.591Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.606Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.681Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.727Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.730Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.760Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Final failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.762Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.770Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:19.889Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.513Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.515Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.516Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.529Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.550Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.555Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:20.661Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.265Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.267Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.269Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.286Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.306Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.335Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Second attempt failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.337Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.356Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.360Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:21.469Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.044Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.046Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.046Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.058Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.078Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.090Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.110Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.132Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.142Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.163Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.184Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.205Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.240Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.272Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.296Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.336Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.365Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.429Z warn: Notification moved to dead letter queue +{"id":4,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.453Z info: Notification marked for retry or failed +{"id":4,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.484Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.532Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.566Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.620Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.669Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.718Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.772Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.812Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.857Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.869Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.888Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.907Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.937Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.939Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.967Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.969Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:22.980Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.000Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.011Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.042Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.065Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.078Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.099Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.121Z info: Notification marked for retry or failed +{"id":11,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.144Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.155Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.177Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.200Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.230Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.233Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.267Z warn: Notification moved to dead letter queue +{"id":12,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.268Z info: Notification marked for retry or failed +{"id":12,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.286Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.312Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.326Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.415Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.439Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.450Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.481Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.504Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.531Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.548Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.570Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.595Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.633Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.636Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.684Z warn: Notification moved to dead letter queue +{"id":16,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.688Z info: Notification marked for retry or failed +{"id":16,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.708Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.867Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.882Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.909Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.931Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.944Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.966Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:23.992Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.026Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.041Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.067Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.096Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.125Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.127Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.160Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.162Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.173Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.199Z info: Notification marked as completed {"id":21} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.212Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.235Z info: Notification marked for retry or failed +{"id":22,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.257Z info: Notification marked as completed {"id":22} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.278Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.301Z info: Notification marked for retry or failed +{"id":23,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.325Z info: Notification marked for retry or failed +{"id":23,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.345Z info: Notification marked as completed {"id":23} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.357Z info: Scheduled notification created {"id":24,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.419Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.466Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.496Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.498Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.528Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.529Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.541Z info: Scheduled notification created {"id":25,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.562Z info: Notification marked as completed {"id":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.575Z info: Scheduled notification created {"id":26,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.596Z info: Notification marked for retry or failed +{"id":26,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.617Z info: Notification marked as completed {"id":26} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.629Z info: Scheduled notification created {"id":27,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.650Z info: Notification marked for retry or failed +{"id":27,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.671Z info: Notification marked for retry or failed +{"id":27,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.692Z info: Notification marked as completed {"id":27} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.704Z info: Scheduled notification created {"id":28,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.727Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.756Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.804Z warn: Notification moved to dead letter queue +{"id":28,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.806Z info: Notification marked for retry or failed +{"id":28,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.855Z warn: Notification moved to dead letter queue +{"id":28,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.857Z info: Notification marked for retry or failed +{"id":28,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.869Z info: Scheduled notification created {"id":29,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.892Z info: Notification marked as completed {"id":29} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.902Z info: Scheduled notification created {"id":30,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.924Z info: Notification marked for retry or failed +{"id":30,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.944Z info: Notification marked as completed {"id":30} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.956Z info: Scheduled notification created {"id":31,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:24.978Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.000Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.020Z info: Notification marked as completed {"id":31} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.032Z info: Scheduled notification created {"id":32,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.054Z info: Notification marked for retry or failed +{"id":32,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.076Z info: Notification marked for retry or failed +{"id":32,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.107Z warn: Notification moved to dead letter queue +{"id":32,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.109Z info: Notification marked for retry or failed +{"id":32,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.140Z warn: Notification moved to dead letter queue +{"id":32,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.142Z info: Notification marked for retry or failed +{"id":32,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.154Z info: Scheduled notification created {"id":33,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.175Z info: Notification marked as completed {"id":33} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.186Z info: Scheduled notification created {"id":34,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.207Z info: Notification marked for retry or failed +{"id":34,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.228Z info: Notification marked as completed {"id":34} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.240Z info: Scheduled notification created {"id":35,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.263Z info: Notification marked for retry or failed +{"id":35,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.285Z info: Notification marked for retry or failed +{"id":35,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.309Z info: Notification marked as completed {"id":35} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.320Z info: Scheduled notification created {"id":36,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.341Z info: Notification marked for retry or failed +{"id":36,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.363Z info: Notification marked for retry or failed +{"id":36,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.394Z warn: Notification moved to dead letter queue +{"id":36,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.396Z info: Notification marked for retry or failed +{"id":36,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.433Z warn: Notification moved to dead letter queue +{"id":36,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.435Z info: Notification marked for retry or failed +{"id":36,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.446Z info: Scheduled notification created {"id":37,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.467Z info: Notification marked as completed {"id":37} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.478Z info: Scheduled notification created {"id":38,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.503Z info: Notification marked for retry or failed +{"id":38,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.524Z info: Notification marked as completed {"id":38} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.549Z info: Scheduled notification created {"id":39,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.571Z info: Notification marked for retry or failed +{"id":39,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.594Z info: Notification marked for retry or failed +{"id":39,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.615Z info: Notification marked as completed {"id":39} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.627Z info: Scheduled notification created {"id":40,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.648Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.670Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.703Z warn: Notification moved to dead letter queue +{"id":40,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.705Z info: Notification marked for retry or failed +{"id":40,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.735Z warn: Notification moved to dead letter queue +{"id":40,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.737Z info: Notification marked for retry or failed +{"id":40,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.749Z info: Scheduled notification created {"id":41,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.772Z info: Notification marked as completed {"id":41} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.785Z info: Scheduled notification created {"id":42,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.807Z info: Notification marked for retry or failed +{"id":42,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.831Z info: Notification marked as completed {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.842Z info: Scheduled notification created {"id":43,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.864Z info: Notification marked for retry or failed +{"id":43,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.887Z info: Notification marked for retry or failed +{"id":43,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.911Z info: Notification marked as completed {"id":43} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.939Z info: Scheduled notification created {"id":44,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.964Z info: Notification marked for retry or failed +{"id":44,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:25.985Z info: Notification marked for retry or failed +{"id":44,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.014Z warn: Notification moved to dead letter queue +{"id":44,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.016Z info: Notification marked for retry or failed +{"id":44,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.047Z warn: Notification moved to dead letter queue +{"id":44,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.048Z info: Notification marked for retry or failed +{"id":44,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.059Z info: Scheduled notification created {"id":45,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.080Z info: Notification marked as completed {"id":45} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.093Z info: Scheduled notification created {"id":46,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.113Z info: Notification marked for retry or failed +{"id":46,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.135Z info: Notification marked as completed {"id":46} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.147Z info: Scheduled notification created {"id":47,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.168Z info: Notification marked for retry or failed +{"id":47,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.191Z info: Notification marked for retry or failed +{"id":47,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.218Z info: Notification marked as completed {"id":47} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.229Z info: Scheduled notification created {"id":48,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.250Z info: Notification marked for retry or failed +{"id":48,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.272Z info: Notification marked for retry or failed +{"id":48,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.305Z warn: Notification moved to dead letter queue +{"id":48,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.306Z info: Notification marked for retry or failed +{"id":48,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.337Z warn: Notification moved to dead letter queue +{"id":48,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.338Z info: Notification marked for retry or failed +{"id":48,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.351Z info: Scheduled notification created {"id":49,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.372Z info: Notification marked as completed {"id":49} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.384Z info: Scheduled notification created {"id":50,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.405Z info: Notification marked for retry or failed +{"id":50,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.425Z info: Notification marked as completed {"id":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.438Z info: Scheduled notification created {"id":51,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.459Z info: Notification marked for retry or failed +{"id":51,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.481Z info: Notification marked for retry or failed +{"id":51,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.504Z info: Notification marked as completed {"id":51} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.516Z info: Scheduled notification created {"id":52,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.538Z info: Notification marked for retry or failed +{"id":52,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.559Z info: Notification marked for retry or failed +{"id":52,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.591Z warn: Notification moved to dead letter queue +{"id":52,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.593Z info: Notification marked for retry or failed +{"id":52,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.623Z warn: Notification moved to dead letter queue +{"id":52,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.625Z info: Notification marked for retry or failed +{"id":52,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.638Z info: Scheduled notification created {"id":53,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.659Z info: Notification marked as completed {"id":53} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.671Z info: Scheduled notification created {"id":54,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.692Z info: Notification marked for retry or failed +{"id":54,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.714Z info: Notification marked as completed {"id":54} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.727Z info: Scheduled notification created {"id":55,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.748Z info: Notification marked for retry or failed +{"id":55,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.770Z info: Notification marked for retry or failed +{"id":55,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.793Z info: Notification marked as completed {"id":55} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.806Z info: Scheduled notification created {"id":56,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.831Z info: Notification marked for retry or failed +{"id":56,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.854Z info: Notification marked for retry or failed +{"id":56,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.887Z warn: Notification moved to dead letter queue +{"id":56,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.888Z info: Notification marked for retry or failed +{"id":56,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.920Z warn: Notification moved to dead letter queue +{"id":56,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.922Z info: Notification marked for retry or failed +{"id":56,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.934Z info: Scheduled notification created {"id":57,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.963Z info: Notification marked as completed {"id":57} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.974Z info: Scheduled notification created {"id":58,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:26.994Z info: Notification marked for retry or failed +{"id":58,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.014Z info: Notification marked as completed {"id":58} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.025Z info: Scheduled notification created {"id":59,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.047Z info: Notification marked for retry or failed +{"id":59,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.058Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.060Z error: Database run error {"sql":"\n UPDATE scheduled_notifications\n + SET \n status = ?,\n retry_count = ?,\n last_error = ?,\n error_details = ?,\n +next_retry_at = ?,\n processing_completed_at = ?,\n updated_at = ?,\n processor_id = NULL,\n + lock_expires_at = NULL\n WHERE id = ?\n ","params":["PENDING",2,"Attempt 2 failed","{\"message\":\"Attempt 2 +failed\",\"stack\":\"Error: Attempt 2 failed\\n at Object. (C:\\\\Users\\\\NEW +OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.t +est.ts:254:15)\",\"timestamp\":\"2026-10-16T23:55:27.057Z\"}",null,null,"2026-10-16T23:55:27.057Z",59],"error":"{\"errn +o\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.061Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.174Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.177Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.178Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.179Z error: Database run error {"sql":"\n UPDATE scheduled_notifications\n + SET \n status = ?,\n retry_count = ?,\n last_error = ?,\n error_details = ?,\n +next_retry_at = ?,\n processing_completed_at = ?,\n updated_at = ?,\n processor_id = NULL,\n + lock_expires_at = NULL\n WHERE id = ?\n ","params":["PENDING",2,"Attempt 2 failed","{\"message\":\"Attempt 2 +failed\",\"stack\":\"Error: Attempt 2 failed\\n at Object. (C:\\\\Users\\\\NEW +OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.t +est.ts:254:15)\",\"timestamp\":\"2026-10-16T23:55:27.057Z\"}",null,null,"2026-10-16T23:55:27.057Z",59],"error":"{\"errn +o\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.780Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.782Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.783Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.794Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.815Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.827Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.838Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.851Z info: Fetched and locked pending notifications +{"count":1,"processorId":"test-processor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.854Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.967Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.985Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.987Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:27.988Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.007Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.029Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.040Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.052Z info: Notification cancelled {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.056Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.165Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.180Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.181Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.183Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.195Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.207Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.211Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.321Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.341Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.343Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.344Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.416Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.427Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.466Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.496Z info: Notification marked as completed {"id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.499Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.614Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.633Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.636Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.638Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.650Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.676Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.742Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.791Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":3,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.822Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":4,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.867Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":5,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:28.952Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":6,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.017Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":7,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.052Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":8,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.259Z warn: Notification moved to dead letter queue +{"id":9,"notificationType":"discord","failureReason":"Attempt 9 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.263Z info: Notification marked for retry or failed +{"id":9,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.329Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.367Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.585Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.604Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.606Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.607Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.620Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.642Z info: Notification marked as completed {"id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.673Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.774Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.861Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:29.982Z info: Notification marked as completed {"id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.030Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.178Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.255Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.373Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.411Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.459Z info: Notification marked for retry or failed +{"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.502Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.522Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.604Z info: Notification marked for retry or failed +{"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.680Z info: Notification marked as completed {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.732Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.895Z info: Notification marked for retry or failed +{"id":17,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:30.982Z info: Notification marked as completed {"id":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.012Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.081Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.155Z info: Notification marked for retry or failed +{"id":18,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.232Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.274Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.330Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.375Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.448Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.478Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.670Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.782Z info: Notification marked for retry or failed +{"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.869Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.875Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.971Z warn: Notification moved to dead letter queue +{"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:31.978Z info: Notification marked for retry or failed +{"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:32.025Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/services/discord-notification-refactored.test.ts (90.246 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   243 | const result = await +service.sendTestMessage(); +  244 | + > 245 | expect(result).toBe(true); +  | ^ +  246 | }); +  247 | +  248 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification-refactored.test.ts:245:22) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/services/scheduled-notification-queue.test.ts (8.77 s) + ● Console + + console.log + 2026-10-16T23:55:36.092Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.007Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.019Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.032Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.069Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.090Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.107Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.133Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.145Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.151Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.181Z info: Fetched and locked pending notifications +{"count":1,"processorId":"restarted-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.191Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:37.209Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.005Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.011Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.017Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.050Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.070Z info: Fetched and locked pending notifications +{"count":1,"processorId":"doomed-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.088Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.097Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.117Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.119Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.123Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.167Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.196Z info: Fetched and locked pending notifications +{"count":1,"processorId":"next-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.202Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:38.319Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.004Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.006Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.007Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.024Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.038Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.049Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.051Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.057Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.077Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.079Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.080Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.083Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.090Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.705Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.727Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.729Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.821Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.836Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.853Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.873Z info: Fetched and locked pending notifications +{"count":3,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.879Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:39.918Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.694Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.696Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.699Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.719Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.736Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.755Z info: Fetched and locked pending notifications +{"count":2,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.880Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.892Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:40.900Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.585Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.593Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.601Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.641Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.672Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.791Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.840Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:41.984Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:42.823Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:42.854Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:42.858Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:42.921Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:42.979Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:43.021Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:56:42.981Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:43.202Z info: Fetched due retries {"count":1,"processorId":"retry-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:43.209Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:43.284Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.035Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.037Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.039Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.125Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.167Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.253Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent outage"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.262Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:44.305Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/services/concurrent-event-processing.test.ts (18.443 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/execution-metrics.test.ts (12.653 s) + ● Console + + console.log + 2026-10-16T23:55:45.308Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.025Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.036Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.047Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.065Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.166Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.243Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.375Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.404Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.542Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.339Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.342Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.353Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.428Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.526Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.558Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.618Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.705Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.731Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.973Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.100Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.102Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.230Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.232Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.280Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.483Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.185Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.187Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.188Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.203Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.226Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.239Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.266Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.286Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.314Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.344Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.382Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.482Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.515Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.529Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.569Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.610Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.636Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.680Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.749Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.751Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.810Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.812Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.831Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.981Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.011Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.014Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.274Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.929Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.961Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.063Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.207Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.337Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.391Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.398Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.413Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.546Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.423Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.477Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.480Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:55.050Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.233Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.143Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.147Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.150Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.171Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.189Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.220Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.226Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/api/execution-metrics-api.test.ts (11.5 s) + ● Console + + console.log + 2026-10-16T23:55:49.833Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.723Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.776Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.786Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.834Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.972Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.017Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.105Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.876Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.988Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.669Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.674Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.675Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.720Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.853Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.901Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.009Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.155Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.254Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.307Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.360Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.362Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.407Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.413Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.453Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.462Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.345Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.348Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.353Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.655Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.692Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.566Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.587Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.592Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.642Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.651Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.509Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.511Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.516Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.563Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.619Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.700Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.903Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/api/queue-operational-metrics.test.ts (23.04 s) + ● Console + + console.log + 2026-10-16T23:55:45.959Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194945831-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.643Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.646Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.655Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194945831-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.721Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:46.751Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194946743-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.538Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.550Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.552Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194946743-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.569Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.598Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.607Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:47.615Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194947610-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.289Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.291Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.293Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194947610-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.339Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.452Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.605Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:48.651Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194948627-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.408Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.411Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.416Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194948627-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.468Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.484Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.498Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:49.505Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194949502-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.299Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.301Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.303Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194949502-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.364Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.501Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.691Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:50.694Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.023Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:51.484Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194951432-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.224Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.229Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.232Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194951432-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.241Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.359Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194952319-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:52.998Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.001Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.003Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194952319-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.043Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.102Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.214Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.361Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"c1bfc9e0","correlationId":"3f46fd98-1fb6-493d-b929-8ceef0274ab3","durationMs":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.445Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:53.460Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194953456-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.360Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.364Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.372Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194953456-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:54.445Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"b3532256","correlationId":"d813ab1f-3e4c-419a-adcc-dee8f22042ba","durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:55.451Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"f25f3980","correlationId":"d009854c-c5af-40fe-bb73-5f296a180c96","durationMs":527} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:55.583Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:55.596Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194955589-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.684Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.697Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.700Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194955589-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.730Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:56.746Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194956736-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.555Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.561Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.562Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194956736-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.618Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.792Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:57.817Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194957811-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.447Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.454Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.459Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792194957811-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.484Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.535Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.638Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:58.654Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +FAIL src/tests/payload-integrity.test.ts + ● Console + + console.log + 2026-10-16T23:55:58.704Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.418Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.425Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.441Z info: Database initialized successfully {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.450Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.553Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.740Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.763Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.880Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.902Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.934Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.951Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:55:59.961Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +ts-jest[ts-compiler] (WARN) src/services/retry-policy-integration.test.ts:631:7 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +631 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:658:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +658 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:680:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +680 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:702:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +702 baseDelayMs: 10, +   ~~~~~~~~~~~ +FAIL src/api/events-server.test.ts (6.301 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/tests/template-api-integration.test.ts (5.565 s) + ● Console + + console.log + 2026-10-16T23:56:03.944Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.564Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.566Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.572Z info: Database initialized successfully {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.575Z error: Database run error {"sql":"duplicate inserts with the same key are +silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.580Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue +(\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n +notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n +target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details +TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count +INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES +scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.584Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS +update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n +UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.586Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.588Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE +notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.589Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.591Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.593Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.595Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.597Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.612Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.616Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.619Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.622Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.625Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.643Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.646Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.649Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.652Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.656Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.658Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.661Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.664Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.666Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.670Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.673Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.676Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.679Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.683Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.687Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.690Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.692Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.695Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.698Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.702Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.706Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.708Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.711Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.714Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.716Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.719Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.722Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.726Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:56:04.730Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/retry-policy-integration.test.ts (6.544 s) + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/__tests__/stress.test.ts (297.53 s) + ● Console + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 1,000 Concurrent Events + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 309ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 3236.25 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 9.08ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 9.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 296.94 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 302.96 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 6.03 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 430.78 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 405.55 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -25.23 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 5,000 Events Sustained Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 1555ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 3215.43 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 33.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 8.37ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 7.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 32.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 305.67 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 312.56 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 6.89 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 406.82 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 425.13 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 18.31 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 10,000 Events - System Stability + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 4075ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 2453.99 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 20.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 7.72ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 7.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 19.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 315.20 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 317.62 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 2.42 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 425.26 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 397.74 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -27.52 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + Sending first burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:356:15) + + console.log + Waiting idle period (2 seconds)... + + at Object. (src/__tests__/stress.test.ts:373:15) + + console.log + Sending second burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:377:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Burst Traffic (2x2000 events) + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 3184ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1256.28 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 9.70ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 11.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 321.15 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 344.91 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 23.76 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 397.82 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 397.11 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -0.71 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + First burst time: 614ms + + at Object. (src/__tests__/stress.test.ts:406:15) + + console.log + Second burst time: 562ms + + at Object. (src/__tests__/stress.test.ts:407:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Priority Queue Under Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 861ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 2322.88 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 20.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 10.25ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 11.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 346.89 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 360.76 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 13.87 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 397.21 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 398.15 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 0.95 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Priority Analysis: + + at Object. (src/__tests__/stress.test.ts:492:15) + + console.log + High-priority in first 500: 500 / 500 (100.00%) + + at Object. (src/__tests__/stress.test.ts:493:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Deduplication Test (3000 events, 1500 unique) + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 3000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 1500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 1449ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 2070.39 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 5.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 12.18ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 12.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 362.36 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 326.96 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: -35.41 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 398.27 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 401.05 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 2.78 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Deduplication Efficiency: + + at Object. (src/__tests__/stress.test.ts:563:15) + + console.log + Total Enqueued: 3000 + + at Object. (src/__tests__/stress.test.ts:564:15) + + console.log + Unique Events: 1500 + + at Object. (src/__tests__/stress.test.ts:565:15) + + console.log + Actually Processed: 1500 + + at Object. (src/__tests__/stress.test.ts:566:15) + + console.log + Duplicates Blocked: 1500 + + at Object. (src/__tests__/stress.test.ts:567:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Discord Notification Service Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 45ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 11111.11 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 13.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 44.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 26.82ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 26.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 39.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 40.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 328.56 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 353.76 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 25.20 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 401.10 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 401.62 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 0.52 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Discord Service Metrics: + + at Object. (src/__tests__/stress.test.ts:630:15) + + console.log + Total Sent: undefined + + at Object. (src/__tests__/stress.test.ts:631:15) + + console.log + Duplicates Skipped: undefined + + at Object. (src/__tests__/stress.test.ts:632:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Error Recovery Under Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 900 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 267577ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 3.74 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 4.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 18.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 10.97ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 11.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 16.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 355.17 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 301.05 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: -54.12 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 401.64 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 3.45 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -398.18 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Error Recovery Statistics: + + at Object. (src/__tests__/stress.test.ts:709:15) + + console.log + Initial Failures: 300 + + at Object. (src/__tests__/stress.test.ts:710:15) + + console.log + Successful Retries: 200 + + at Object. (src/__tests__/stress.test.ts:711:15) + + console.log + Final Success Rate: 90.00% + + at Object. (src/__tests__/stress.test.ts:712:15) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 950 + Received: 900 + +   712 | console.log(` Final Success Rate: ${((processedIds.length / +eventCount) * 100).toFixed(2)}%`); +  713 | + > 714 | +expect(processedIds.length).toBeGreaterThanOrEqual(eventCount * +0.95); // At least 95% success +  | ^ +  715 | expect(retryCount).toBeGreaterThan(0); // Verify +retries occurred +  716 | }, 180000); +  717 | }); + + at Object. (src/__tests__/stress.test.ts:714:35) + +FAIL src/services/notification-failure-scenarios.test.ts (183.347 s) + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › tracks timeoutCount in DiscordNotificationService on +AbortError + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 1 + Received: 0 + +   71 | const success = await +service.sendTestMessage(); +  72 | expect(success).toBe(false); + > 73 | +expect(service.getMetrics().timeoutCount).toBeGreaterThanOrEqual(1); +  | ^ +  74 | }); +  75 | }); +  76 | + + at Object. (src/services/notification-failure-scenarios.test.ts:73:49) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/webhook-retry-helper.test.ts (194.791 s) + ● sendWebhookWithRetry › network/connection errors are retried › succeeds after a network error is resolved on retry + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   195 | }); +  196 | + > 197 | it('succeeds after a network error is resolved on +retry', async () => { +  | ^ +  198 | mockSendWebhook +  199 | .mockRejectedValueOnce(makeNetworkError()) +  200 | .mockResolvedValueOnce(makeResponse(200)); + + at src/services/webhook-retry-helper.test.ts:197:7 + at src/services/webhook-retry-helper.test.ts:184:11 + at Object. (src/services/webhook-retry-helper.test.ts:61:9) + + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/tests/template-system.test.ts (5.587 s) + ● Console + + console.log + 2026-10-16T23:59:23.602Z info: Connected to SQLite database {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:25.208Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:25.253Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:25.295Z info: Database initialized successfully {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:25.318Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX +IF NOT EXISTS idx_templates_unique_key \r\n ON +notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:25.545Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/database/event-notification-indexes.test.ts + ● Console + + console.log + 2026-10-16T23:59:27.049Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-16188-1792195167036.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:28.298Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:28.303Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:28.315Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-16188-1792195167036.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:28.354Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, +created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILE +D","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:28.378Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/services/webhook-delivery-service.test.ts (26.9 s) + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 1, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +ts-jest[ts-compiler] (WARN) src/services/webhook-verifier.test.ts:601:20 - +error TS2339: Property 'authenticated' does not exist on type 'Promise'. + +601 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:601:20 + 601 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:602:20 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +602 expect(outcome.statusCode).toBe(401); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:602:20 + 602 expect(outcome.statusCode).toBe(401); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:603:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +603 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:603:20 + 603 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:613:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +613 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:613:20 + 613 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:628:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +628 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:628:20 + 628 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:639:21 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +639 expect(replay.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:639:21 + 639 expect(replay.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:640:21 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +640 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:640:21 + 640 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:657:18 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +657 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:657:18 + 657 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:668:19 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +668 expect(second.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:668:19 + 668 expect(second.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:669:19 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +669 expect(second.statusCode).toBe(409); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:669:19 + 669 expect(second.statusCode).toBe(409); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:670:19 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +670 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:670:19 + 670 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:695:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +695 expect(a.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:695:14 + 695 expect(a.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:696:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +696 expect(b.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:696:14 + 696 expect(b.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:709:16 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +709 expect(bad.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:709:16 + 709 expect(bad.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:718:17 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +718 expect(good.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:718:17 + 718 expect(good.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:729:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +729 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:729:20 + 729 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:730:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +730 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:730:20 + 730 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:740:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +740 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:740:20 + 740 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:741:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +741 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:741:20 + 741 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:756:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +756 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:756:20 + 756 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:770:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +770 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:770:20 + 770 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +FAIL src/services/webhook-verifier.test.ts (28.135 s) + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-backfill.test.ts:51:3 - +error TS1117: An object literal cannot have multiple properties with the same name. + +51 validateRpcResponse: jest.fn().mockReturnValue({ valid: true }), +   ~~~~~~~~~~~~~~~~~~~ +src/services/event-subscriber-backfill.test.ts:83:3 - error TS2741: +Property 'eventBatchSize' is missing in type '{ stellarNetwork: string; stellarNetworkPassphrase: string; +stellarRpcUrl: string; contractAddresses: ContractConfig[]; pollIntervalMs: number; maxReconnectAttempts: number; +reconnectDelayMs: number; eventsApiPort: number; eventsApiCorsOrigin: string; backfill: { ...; } | undefined; }' but +required in type 'Config'. + +83 return { +   ~~~~~~ + + src/types/index.ts:72:3 + 72 eventBatchSize: number; +    ~~~~~~~~~~~~~~ + 'eventBatchSize' is declared here. +FAIL src/tests/api-versioning.test.ts (25.583 s) + ● Console + + console.log + 2026-10-16T23:59:35.333Z info: Handling GET /api/events +{"requestId":"5bbc4546","correlationId":"e0fa6cd0-a109-487f-af48-0f7535be0d8e","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.545Z info: GET /api/events complete +{"requestId":"5bbc4546","correlationId":"e0fa6cd0-a109-487f-af48-0f7535be0d8e","returned":0,"durationMs":227} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.774Z info: Handling GET /api/events +{"requestId":"9ee35afc","correlationId":"c7680885-097a-4b56-8d08-64ceeb264dde","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.783Z info: GET /api/events complete +{"requestId":"9ee35afc","correlationId":"c7680885-097a-4b56-8d08-64ceeb264dde","returned":0,"durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.805Z info: Handling GET /api/events +{"requestId":"078d8ebb","correlationId":"fc3af687-631e-4007-971e-e2331b3942cd","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.809Z info: GET /api/events complete +{"requestId":"078d8ebb","correlationId":"fc3af687-631e-4007-971e-e2331b3942cd","returned":0,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.816Z info: Handling GET /api/events +{"requestId":"5df5659d","correlationId":"eb4d487c-c93c-4b8a-aec9-5e3219faa9ae","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.817Z info: GET /api/events complete +{"requestId":"5df5659d","correlationId":"eb4d487c-c93c-4b8a-aec9-5e3219faa9ae","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.836Z info: Handling GET /api/events +{"requestId":"0022215e","correlationId":"ab04f69e-c359-4b09-9c85-113a74b563c6","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.842Z info: GET /api/events complete +{"requestId":"0022215e","correlationId":"ab04f69e-c359-4b09-9c85-113a74b563c6","returned":0,"durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.878Z info: Handling GET /api/events +{"requestId":"b2d85634","correlationId":"fd9ff6f6-6526-4c82-a200-1f8fbb9e8be1","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.881Z info: GET /api/events complete +{"requestId":"b2d85634","correlationId":"fd9ff6f6-6526-4c82-a200-1f8fbb9e8be1","returned":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.886Z info: Handling GET /api/events +{"requestId":"dc976a87","correlationId":"c5766a07-a816-4f72-a776-e606b316ac26","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.887Z info: GET /api/events complete +{"requestId":"dc976a87","correlationId":"c5766a07-a816-4f72-a776-e606b316ac26","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.913Z info: Handling GET /api/events +{"requestId":"9fb89a81","correlationId":"0ade22d5-0c93-4ca8-8fa3-0f37d7648a7c","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.916Z info: GET /api/events complete +{"requestId":"9fb89a81","correlationId":"0ade22d5-0c93-4ca8-8fa3-0f37d7648a7c","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.921Z info: Handling GET /api/events +{"requestId":"e1d85904","correlationId":"4a3f0ce4-45e2-4134-9a34-160aa51b48fd","limit":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.924Z info: GET /api/events complete +{"requestId":"e1d85904","correlationId":"4a3f0ce4-45e2-4134-9a34-160aa51b48fd","returned":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.948Z warn: Unhandled request {"requestId":"a0513bcc","correlationId":"ead3f840-bf5e +-4502-ae9f-5e703ef1cb20","method":"GET","url":"/api/v1/unknown-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.954Z info: Request completed +{"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":9,"requestId":"a0513bcc"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.982Z warn: Unhandled request {"requestId":"7405f48e","correlationId":"e9fe24e8-229b +-4584-b726-ab6851fd7c55","method":"GET","url":"/api/v1/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:35.986Z info: Request completed +{"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":4,"requestId":"7405f48e"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/services/event-subscriber-backfill.test.ts (8.01 s) + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/services/retry-scheduler-webhook.test.ts (6.595 s) + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +FAIL src/database/database-transaction.test.ts (6.343 s) + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/api/template-routes.test.ts (10.114 s) + ● Console + + console.log + 2026-10-16T23:59:46.060Z error: Failed to create template {"error":{"message":"uniqueKey: is required +and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is +required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: uniqueKey: is required and +must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required +and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:85:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.217Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:107:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.233Z error: Failed to create template {"error":{"message":"Invalid JSON +body","name":"Error","stack":"Error: Invalid JSON body\n at EventEmitter. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:42:16)\n at +EventEmitter.emit (node:events:509:28)\n at C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:26:9\n at processTicksAndRejections +(node:internal/process/task_queues:85:11)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.240Z error: Failed to create template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:141:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:137:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.261Z error: Failed to create template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:150:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:153:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.310Z error: Failed to list templates {"error":{"message":"channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.319Z info: Listed templates via API +{"requestId":"req-1","count":0,"channelType":"EMAIL","activeOnly":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.367Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:201:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.376Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:215:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.405Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:288:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:226:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.417Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:237:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.444Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:247:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.452Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:360:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:261:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.484Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:46.501Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +ts-jest[ts-compiler] (WARN) src/__tests__/smoke/notification-pipeline.smoke.test.ts:602:5 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +602 contractId: contractAddress, +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +ts-jest[ts-compiler] (WARN) src/services/archive.test.ts:237:7 - error +TS2741: Property 'expiresAt' is missing in type '{ originalId: number; payload: string; notificationType: string; +targetRecipient: string; executeAt: string; createdAt: string; processingCompletedAt: string; status: string; +retryCount: number; lastError: null; eventId: null; contractAddress: null; metadata: null; }' but required in type '{ +originalId: number; payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: +string | null; createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +237 { +   ~ +238 originalId: 1, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +250 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +251 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +src/services/archive.test.ts:252:7 - error TS2741: Property 'expiresAt' +is missing in type '{ originalId: number; payload: string; notificationType: string; targetRecipient: string; +executeAt: string; createdAt: string; processingCompletedAt: null; status: string; retryCount: number; lastError: +string; eventId: string; contractAddress: string; metadata: null; }' but required in type '{ originalId: number; +payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: string | null; +createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +252 { +   ~ +253 originalId: 2, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +265 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +266 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts (11.368 s) + ● Console + + console.log + 2026-10-16T23:59:48.367Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.406Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.476Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.538Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.553Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/services/archive.test.ts (7.916 s) + ● Console + + console.log + 2026-10-16T23:59:48.672Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T2 +3:59:48.670Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.730Z info: ArchiveService: cycle complete +{"archived":2,"purged":0,"durationMs":59,"jobId":"archive-cycle-1792195188664"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.733Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.800Z info: ArchiveService: archived processed notification immediately {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.815Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":7,"jobId":"archive-cycle-1792195188804"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.819Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.827Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":5,"jobId":"archive-cycle-1792195188822"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.830Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.920Z info: ArchiveService: purged expired archive records +{"count":2,"cutoff":"2026-07-18T23:59:48.899Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.928Z info: ArchiveService: cycle complete +{"archived":0,"purged":2,"durationMs":34,"jobId":"archive-cycle-1792195188881"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.932Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.944Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792195188943"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.950Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.954Z info: ArchiveService started +{"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.954Z info: Waiting for active archival jobs to complete {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:48.954Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:49.070Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":116,"jobId":"archive-cycle-1792195188954"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:49.072Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:49.104Z info: Handling GET /api/archive {"requestId":"req-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:49.147Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:49.154Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:49.177Z info: Handling POST /api/archive/run {"requestId":"req-6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber.test.ts:739:9 - +error TS2353: Object literal may only specify known properties, and 'receivedAt' does not exist in +type 'Partial'. + +739 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:772:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +772 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:799:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +799 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:835:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +835 receivedAt: veryOldTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:861:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +861 receivedAt: NOW - (365 * 24 * 60 * 60 * 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:884:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +884 receivedAt: NOW - (DEFAULT_EXPIRATION_MS + 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:888:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +888 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:924:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +924 receivedAt: fastEventExpiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:928:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +928 receivedAt: NOW - 60 * 60 * 1000, // 1 hour ago (within default 24h) +   ~~~~~~~~~~ +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-reorg.test.ts:62:3 - +error TS2353: Object literal may only specify known properties, and 'maxPayloadSizeBytes' does not +exist in type 'Config'. + +62 maxPayloadSizeBytes: 64 * 1024, +   ~~~~~~~~~~~~~~~~~~~ +FAIL src/services/event-subscriber.test.ts (16.035 s) + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792108790187} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792195191738, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792108790187} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792195191738, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "f363ab27"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +ts-jest[ts-compiler] (WARN) src/services/notification-retry-queue.test.ts:52:66 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +52 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:62:66 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +62 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:75:66 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +75 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:96:9 - error TS2353: Object +literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +96 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:120:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +120 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:139:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +139 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:161:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +161 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:188:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +188 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:214:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +214 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:248:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +248 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:272:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +272 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:288:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +288 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:326:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +326 baseDelayMs: 100, +   ~~~~~~~~~~~ +FAIL src/services/event-subscriber-reorg.test.ts (6.879 s) + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "537ee96d"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "7603e27b-8fdf-4bf6-9238-3f2eb407c5ee", +"eventId": "event-1", "requestId": "7603e27b-8fdf-4bf6-9238-3f2eb407c5ee"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/services/notification-retry-queue.test.ts (5.007 s) + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4660.864163126824, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:59:58.339Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/__tests__/retry-scheduler.integration.test.ts (9.224 s) + ● Console + + console.log + 2026-10-16T23:59:53.757Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:54.627Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:55.289Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:56.508Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:59:57.640Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/providers/provider-capabilities.test.ts (7.072 s) + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/__tests__/config-startup-validation.test.ts (6.347 s) + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +ts-jest[ts-compiler] (WARN) src/services/notification-retry-queue-refactored.test.ts:39:66 +- error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist +in type 'RetryQueueOptions'. + +39 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:51:66 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +51 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:72:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +72 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:97:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +97 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:117:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +117 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:146:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +146 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:174:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +174 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:205:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +205 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:241:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +241 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:270:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +270 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:287:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +287 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:309:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +309 baseDelayMs: 100, +   ~~~~~~~~~~~ +FAIL src/api/archive-api.test.ts + ● Console + + console.log + 2026-10-17T00:00:02.894Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.963Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an +integer, received 'abc'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.969Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.972Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= +100"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.977Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.980Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= +0"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.983Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.985Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be +one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.989Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.992Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be +a valid date, received 'not-a-date'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:02.996Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.002Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.017Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.022Z info: Handling POST /api/archive/run {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4226.063998797194, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-17T00:00:07.168Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/services/notification-api-webhook.test.ts + ● Console + + console.log + 2026-10-17T00:00:03.713Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.752Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.764Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.778Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.784Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.818Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.824Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.832Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:03.836Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +ts-jest[ts-compiler] (WARN) src/services/notification-expiration.test.ts:40:9 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +40 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:45:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +45 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:46:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +46 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:63:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +63 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:68:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +68 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:69:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +69 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:86:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +86 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:91:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +91 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:92:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +92 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:116:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +116 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:121:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +121 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:122:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +122 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:140:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +140 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:145:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +145 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:146:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +146 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:164:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +164 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:169:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +169 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:170:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +170 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:199:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +199 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:204:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +204 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:205:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +205 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:222:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +222 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:227:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +227 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:228:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +228 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:340:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +340 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:345:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +345 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:346:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +346 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:379:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +379 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:384:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +384 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:385:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +385 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:407:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +407 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:412:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +412 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:413:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +413 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:435:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +435 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:440:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +440 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:441:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +441 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:467:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +467 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:472:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +472 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:473:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +473 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:503:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +503 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:508:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +508 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:509:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +509 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:521:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +521 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:526:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +526 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:527:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +527 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:552:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +552 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:557:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +557 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:558:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +558 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +PASS src/tests/notification-scheduler.test.ts (7.236 s) + ● Console + + console.log + 2026-10-17T00:00:10.230Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.152Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.155Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.168Z info: Database initialized successfully +{"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.207Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.229Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.238Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.245Z info: Processing scheduled notification +{"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.247Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":1058} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.278Z info: Notification marked as completed {"requestId":"test-request-id","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.295Z info: Notification delivered successfully +{"requestId":"test-request-id","id":1,"type":"webhook","durationMs":17} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.297Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-success-test","count":1,"durationMs":86} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.302Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.365Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.386Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.388Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.389Z info: Processing scheduled notification +{"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.392Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":1042} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.395Z error: Failed to process notification +{"requestId":"test-request-id","id":2,"error":{"message":"secret +token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.411Z error: Job monitor: job failed +{"jobId":"notification-2","jobName":"scheduled-notification","error":"secret token=should-not-persist","durationMs":21} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.428Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-17T00:00:16.417Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.440Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":74} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.447Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.495Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.535Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.558Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.584Z info: Expired notification skipped before delivery +{"requestId":"test-request-id","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.633Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.648Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.652Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.654Z info: Processing scheduled notification +{"requestId":"test-request-id","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.657Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":5,"executeAt":{},"now":{},"missedByMs":60039} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.660Z error: Failed to process notification +{"requestId":"test-request-id","id":5,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary +outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.663Z error: Job monitor: job failed +{"jobId":"notification-5","jobName":"scheduled-notification","error":"temporary outage","durationMs":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.677Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-17T00:00:11.764Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.694Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":59} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.733Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.735Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-expiration-test","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.762Z info: Expired retry skipped before delivery +{"requestId":"test-request-id","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.765Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.814Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.828Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.845Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.848Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.850Z info: Processing scheduled notification +{"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.851Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1052} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.868Z info: Notification marked as completed {"requestId":"test-request-id","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.887Z info: Notification delivered successfully +{"requestId":"test-request-id","id":6,"type":"discord","durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.897Z info: Processing scheduled notification +{"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.901Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":1084} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.924Z info: Notification marked as completed {"requestId":"test-request-id","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.946Z info: Notification delivered successfully +{"requestId":"test-request-id","id":7,"type":"discord","durationMs":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:11.952Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":122} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.962Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.049Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.068Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.088Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.162Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.198Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.202Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.204Z info: Processing scheduled notification +{"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:12.206Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60075} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:12.213Z error: Failed to process notification +{"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: +temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:12.220Z error: Job monitor: job failed +{"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:12.247Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-17T00:00:14.222Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:12.263Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":99} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.302Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.304Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.306Z info: Retrying notification +{"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.344Z info: Notification marked as completed {"requestId":"test-request-id","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.361Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.365Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.428Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.447Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.493Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.511Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.548Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.612Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.629Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.675Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.696Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.751Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.785Z warn: Notification moved to dead letter queue +{"id":15,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.786Z info: Notification marked for retry or failed +{"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.837Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.854Z info: Notification cancelled {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.935Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.951Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.004Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.026Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.066Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.081Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.197Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.214Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.261Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.288Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.349Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.395Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.408Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:13.417Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.930Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.932Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.934Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.963Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.976Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.078Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.098Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.146Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.180Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.182Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.222Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.241Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-stale"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.284Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.330Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.351Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.356Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/services/discord-notification.test.ts (13.863 s) + ● DiscordNotificationService › sendEventNotification › should send event notification successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   58 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  59 | + > 60 | expect(result).toBe(true); +  | ^ +  61 | expect(mockFetch).toHaveBeenCalledTimes(1); +  62 | const [url, options] = +mockFetch.mock.calls[0]; +  63 | expect(url).toBe(mockConfig.webhookUrl); + + at Object. (src/services/discord-notification.test.ts:60:22) + + ● DiscordNotificationService › sendEventNotification › should handle request timeout + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Discord webhook request timed out", ObjectContaining {"timeoutMs": 100, "webhookId": "123"} + Received + 1: "Discord webhook request error", {"attempt": 0, "contractAddress": "CA123", "correlationId": +"d0da9553-273a-42ad-acf7-bd7ce982e61c", "durationMs": 101, "error": [DnsRebindingError: DNS resolution returned no +addresses for hostname: discord.com], "eventId": "event-123", "requestId": "d0da9553-273a-42ad-acf7-bd7ce982e61c", +"webhookId": "123"} + 2: "Exceeded max Discord retry attempts", {"contractAddress": "CA123", "correlationId": +"d0da9553-273a-42ad-acf7-bd7ce982e61c", "eventId": "event-123", "maxRetries": 0, "requestId": +"d0da9553-273a-42ad-acf7-bd7ce982e61c", "webhookId": "123"} + + Number of calls: 2 + +   111 | +  112 | expect(result).toBe(false); + > 113 | expect(mockLoggerModule.error).toHaveBeenCalledWith( +  | ^ +  114 | 'Discord webhook request timed out', +  115 | expect.objectContaining({ +  116 | webhookId: mockConfig.webhookId, + + at Object. (src/services/discord-notification.test.ts:113:38) + + ● DiscordNotificationService › duplicate detection › skips the webhook call for a duplicate event + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   133 | const secondResult = await +service.sendEventNotification(mockEvent, mockContractConfig); +  134 | + > 135 | +expect(mockFetch).toHaveBeenCalledTimes(1); +  | ^ +  136 | expect(secondResult).toBe(true); +  137 | expect(service.getDeduplicationMetrics()).toEqual( +  138 | expect.objectContaining({ skippedDuplicates: 1, +cacheSize: 1 }), + + at Object. (src/services/discord-notification.test.ts:135:25) + + ● DiscordNotificationService › duplicate detection › logs a duplicate detection event + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping duplicate notification", ObjectContaining {"contractAddress": "CA123", "eventId": +"event-dup-log"} + Received + 1: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"3de85a7a-ecfb-4a51-8421-5e9cb1e3980a", "eventId": "event-dup-log", "requestId": +"3de85a7a-ecfb-4a51-8421-5e9cb1e3980a", "webhookId": "123"} + 2: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"c1d6d148-ca0e-4ff3-b9cf-58da3bdd693f", "eventId": "event-dup-log", "requestId": +"c1d6d148-ca0e-4ff3-b9cf-58da3bdd693f", "webhookId": "123"} + + Number of calls: 2 + +   151 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  152 | + > 153 | expect(mockLoggerModule.info).toHaveBeenCalledWith( +  | ^ +  154 | 'Skipping duplicate notification', +  155 | expect.objectContaining({ +  156 | eventId: 'event-dup-log', + + at Object. (src/services/discord-notification.test.ts:153:37) + + ● DiscordNotificationService › duplicate detection › allows the same notification request after the configured +window expires + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   173 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  174 | + > 175 | expect(result).toBe(true); +  | ^ +  176 | expect(mockFetch).toHaveBeenCalledTimes(2); +  177 | expect(service.getDeduplicationMetrics()).toEqual( +  178 | expect.objectContaining({ acceptedRequests: 2, +skippedDuplicates: 1 }), + + at Object. (src/services/discord-notification.test.ts:175:22) + + ● DiscordNotificationService › duplicate detection › allows the same event id on a different contract through + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 0 + +   191 | const result = await +service.sendEventNotification(mockEvent, contractB); +  192 | + > 193 | +expect(mockFetch).toHaveBeenCalledTimes(2); +  | ^ +  194 | expect(result).toBe(true); +  195 | }); +  196 | + + at Object. (src/services/discord-notification.test.ts:193:25) + + ● DiscordNotificationService › duplicate detection › does not mark an event as sent when the webhook call fails + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   213 | +  214 | expect(firstResult).toBe(false); + > 215 | expect(secondResult).toBe(true); +  | ^ +  216 | expect(mockFetch).toHaveBeenCalledTimes(2); +  217 | }); +  218 | + + at Object. (src/services/discord-notification.test.ts:215:28) + + ● DiscordNotificationService › duplicate detection › accepts an injected deduplicator + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   227 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  228 | + > 229 | +expect(deduplicator.size()).toBe(1); +  | ^ +  230 | +expect(deduplicator.isDuplicate(generateFingerprint('event-injected', +'CA123'))).toBe(true); +  231 | }); +  232 | }); + + at Object. (src/services/discord-notification.test.ts:229:35) + + ● DiscordNotificationService › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   239 | const result = await +service.sendTestMessage(); +  240 | + > 241 | expect(result).toBe(true); +  | ^ +  242 | }); +  243 | +  244 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification.test.ts:241:22) + + ● DiscordNotificationService › formatEventMessage › should format event with string value correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   264 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  265 | + > 266 | expect(result).toBe(true); +  | ^ +  267 | const [, options] = +mockFetch.mock.calls[0]; +  268 | const body = +JSON.parse(options.body); +  269 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:266:22) + + ● DiscordNotificationService › formatEventMessage › should truncate long string values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   283 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  284 | + > 285 | expect(result).toBe(true); +  | ^ +  286 | const [, options] = +mockFetch.mock.calls[0]; +  287 | const body = +JSON.parse(options.body); +  288 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:285:22) + + ● DiscordNotificationService › formatEventMessage › should handle symbol type values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   302 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  303 | + > 304 | expect(result).toBe(true); +  | ^ +  305 | const [, options] = +mockFetch.mock.calls[0]; +  306 | const body = +JSON.parse(options.body); +  307 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:304:22) + + ● DiscordNotificationService › formatEventMessage › should preserve short messages without truncation + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   320 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  321 | + > 322 | expect(result).toBe(true); +  | ^ +  323 | const [, options] = +mockFetch.mock.calls[0]; +  324 | const body = +JSON.parse(options.body); +  325 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:322:22) + + ● DiscordNotificationService › formatEventMessage › should truncate oversized embed titles + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   340 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  341 | + > 342 | expect(result).toBe(true); +  | ^ +  343 | const [, options] = +mockFetch.mock.calls[0]; +  344 | const body = +JSON.parse(options.body); +  345 | expect(body.embeds[0].title.length).toBeL +essThanOrEqual(256); + + at Object. (src/services/discord-notification.test.ts:342:22) + + ● DiscordNotificationService › formatEventMessage › should enforce total embed size limit + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   361 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  362 | + > 363 | expect(result).toBe(true); +  | ^ +  364 | const [, options] = +mockFetch.mock.calls[0]; +  365 | const body = +JSON.parse(options.body); +  366 | const embed = body.embeds[0]; + + at Object. (src/services/discord-notification.test.ts:363:22) + + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   380 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  381 | + > 382 | expect(result).toBe(true); +  | ^ +  383 | const [, options] = +mockFetch.mock.calls[0]; +  384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:382:22) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone +embedded in an scvString payload + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   459 | +  460 | describe('integration: mention in event string value reaches embed +sanitized', () => { + > 461 | it('strips @everyone embedded in an scvString +payload', async () => { +  | ^ +  462 | mockFetch.mockResolvedValueOnce({ ok: true }); +  463 | +  464 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:461:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone in +event topic symbol used as embed title + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   487 | }); +  488 | + > 489 | it('strips @everyone in event topic symbol used as embed +title', async () => { +  | ^ +  490 | mockFetch.mockResolvedValueOnce({ ok: true }); +  491 | +  492 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:489:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + +PASS src/__tests__/notification-flow-e2e.test.ts (10.836 s) + ● Console + + console.log + 2026-10-17T00:00:11.253Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.902Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.200Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.517Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.629Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.198Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.348Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.457Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.697Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.089Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.917Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.040Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.163Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.290Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.415Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/queue-visibility.test.ts (12.069 s) + ● Console + + console.log + 2026-10-17T00:00:09.543Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.290Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.296Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.303Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.333Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.356Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.378Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.411Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:10.433Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.260Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.263Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.265Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.279Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.284Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.973Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.990Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:11.995Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.004Z info: Handling GET /api/schedule/queue +{"requestId":"b634a4c3","correlationId":"5cb104da-74b0-4489-82d8-782ffc0fd480","count":0,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.018Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.031Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.795Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.797Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.800Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.836Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.845Z info: Handling GET /api/schedule/queue +{"requestId":"e1d1bd9d","correlationId":"3f6befcd-dd20-429b-8bd0-f10a388abcd1","count":1,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.854Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:12.871Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.379Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.381Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.382Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.401Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.418Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.431Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.462Z warn: Notification moved to dead letter queue +{"id":2,"notificationType":"discord","failureReason":"oops"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.464Z info: Notification marked for retry or failed +{"id":2,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.481Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.486Z info: Handling GET /api/schedule/queue +{"requestId":"a071173e","correlationId":"7b69c554-e0b6-4291-ad57-04f98325a293","count":1,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.491Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:14.497Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.456Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.458Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.464Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.482Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.496Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.513Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.531Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.546Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.551Z info: Handling GET /api/schedule/queue +{"requestId":"b0cab143","correlationId":"0e7cb8d9-9081-4003-9b55-bc05c22e12f6","count":3,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.557Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:15.564Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.245Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.247Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.249Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.267Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.272Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.988Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.990Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:16.995Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.012Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.018Z info: Handling GET /api/schedule/queue +{"requestId":"32f07f18","correlationId":"b0e50eaf-777c-42f8-96e3-69cf39ce4712","count":1,"durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.024Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.033Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.831Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.832Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.833Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.847Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.854Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.715Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.717Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.718Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.732Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.747Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.752Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.754Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM +dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.756Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibi +lity.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.769Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.879Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.881Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.629Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.630Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.631Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.637Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.646Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.651Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/database-recovery.test.ts + ● Console + + console.log + 2026-10-17T00:00:17.882Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.728Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.732Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.737Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.755Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('after')","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.758Z warn: Database connection failure detected; attempting recovery +{"path":"./data/test-db-recovery.db","attempt":1,"maxReconnectAttempts":3,"delayMs":10,"error":"SQLITE_MISUSE: +Database is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.791Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.795Z warn: Database connection recovered {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.815Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.821Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.610Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.612Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.614Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.620Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.622Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"maxReconnectAttempts":2,"delayMs":1,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.628Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.633Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.636Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.637Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"maxReconnectAttempts":2,"delayMs":2,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.657Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.665Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.668Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.678Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.502Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.504Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.506Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.531Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('dup')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.534Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.542Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.196Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.198Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.201Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.286Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('kept')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.304Z error: Transaction rolled back +{"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.318Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.336Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.978Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.980Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.982Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.985Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/database/query-performance.test.ts + ● Console + + console.log + 2026-10-17T00:00:17.853Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792195217848.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.740Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.747Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:18.753Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792195217848.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.021Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/scheduler-concurrency.test.ts (7.463 s) + ● Console + + console.log + 2026-10-17T00:00:16.919Z info: Connected to SQLite database +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.747Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.750Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.754Z info: Database initialized successfully +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.763Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.793Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.795Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.811Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.814Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.830Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.832Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:17.846Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.173Z info: Fetched and locked pending notifications +{"requestId":"c4e2bf93","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.178Z info: Processing batch of scheduled notifications +{"requestId":"c4e2bf93","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.181Z info: Processing scheduled notification +{"requestId":"c4e2bf93","id":1,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.285Z info: Notification marked as completed {"requestId":"c4e2bf93","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.310Z info: Notification delivered successfully +{"requestId":"c4e2bf93","id":1,"type":"discord","durationMs":90} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.314Z info: Processing scheduled notification +{"requestId":"c4e2bf93","id":2,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.418Z info: Notification marked as completed {"requestId":"c4e2bf93","id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.434Z info: Notification delivered successfully +{"requestId":"c4e2bf93","id":2,"type":"discord","durationMs":89} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.436Z info: Processing scheduled notification +{"requestId":"c4e2bf93","id":3,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.538Z info: Notification marked as completed {"requestId":"c4e2bf93","id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.562Z info: Notification delivered successfully +{"requestId":"c4e2bf93","id":3,"type":"discord","durationMs":86} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.564Z info: Processing scheduled notification +{"requestId":"c4e2bf93","id":4,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.667Z info: Notification marked as completed {"requestId":"c4e2bf93","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.679Z info: Notification delivered successfully +{"requestId":"c4e2bf93","id":4,"type":"discord","durationMs":91} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:19.681Z info: Scheduler batch complete +{"requestId":"c4e2bf93","processorId":"test-uuid-0001","count":4,"durationMs":529} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.684Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.720Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.738Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.740Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.763Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.765Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.781Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.783Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.802Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.804Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.818Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.820Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:19.837Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.232Z info: Fetched and locked pending notifications +{"requestId":"12d68f38","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.236Z info: Processing batch of scheduled notifications +{"requestId":"12d68f38","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.249Z info: Processing scheduled notification +{"requestId":"12d68f38","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:21.254Z info: Processing scheduled notification +{"requestId":"12d68f38","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:21.256Z info: Processing scheduled notification +{"requestId":"12d68f38","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-17T00:00:21.382Z info: Notification marked as completed {"requestId":"12d68f38","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:21.385Z info: Notification marked as completed {"requestId":"12d68f38","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:21.436Z info: Notification delivered successfully +{"requestId":"12d68f38","id":5,"type":"discord","durationMs":94} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:21.439Z info: Processing scheduled notification +{"requestId":"12d68f38","id":8,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:21.445Z info: Notification marked as completed {"requestId":"12d68f38","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-17T00:00:21.448Z info: Notification delivered successfully +{"requestId":"12d68f38","id":6,"type":"discord","durationMs":92} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:21.450Z info: Processing scheduled notification +{"requestId":"12d68f38","id":9,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:21.478Z info: Notification delivered successfully +{"requestId":"12d68f38","id":7,"type":"discord","durationMs":97} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-17T00:00:21.484Z info: Processing scheduled notification +{"requestId":"12d68f38","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-17T00:00:21.551Z info: Notification marked as completed {"requestId":"12d68f38","id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:21.594Z info: Notification marked as completed {"requestId":"12d68f38","id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:21.597Z info: Notification delivered successfully +{"requestId":"12d68f38","id":8,"type":"discord","durationMs":90} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:21.628Z info: Notification delivered successfully +{"requestId":"12d68f38","id":9,"type":"discord","durationMs":84} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:21.630Z info: Notification marked as completed {"requestId":"12d68f38","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-17T00:00:21.654Z info: Notification delivered successfully +{"requestId":"12d68f38","id":10,"type":"discord","durationMs":113} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-17T00:00:21.660Z info: Scheduler batch complete +{"requestId":"12d68f38","processorId":"test-uuid-0002","count":6,"durationMs":509} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.665Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.702Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.724Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.730Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.748Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.064Z info: Fetched and locked pending notifications +{"requestId":"1446129f","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.066Z info: Processing batch of scheduled notifications +{"requestId":"1446129f","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.069Z info: Processing scheduled notification +{"requestId":"1446129f","id":11,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:23.071Z info: Processing scheduled notification +{"requestId":"1446129f","id":12,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:23.185Z info: Notification marked as completed {"requestId":"1446129f","id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:23.188Z info: Notification marked as completed {"requestId":"1446129f","id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:23.212Z info: Notification delivered successfully +{"requestId":"1446129f","id":11,"type":"discord","durationMs":92} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:23.215Z info: Notification delivered successfully +{"requestId":"1446129f","id":12,"type":"discord","durationMs":93} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-17T00:00:23.217Z info: Scheduler batch complete +{"requestId":"1446129f","processorId":"test-uuid-0003","count":2,"durationMs":165} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.218Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.221Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/circuit-breaker.test.ts + ● Console + + console.log + 2026-10-17T00:00:20.086Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.117Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.122Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.124Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.130Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.134Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.138Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.139Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.143Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195221143} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.146Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.150Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.151Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.153Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195221153} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.155Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.157Z warn: Circuit breaker is OPEN, blocking request +{"state":"open","nextAttemptTime":1792195221153} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.165Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.167Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.169Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195221169} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:20.171Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.278Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.303Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.305Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.307Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195222306} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:21.317Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.421Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.427Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.430Z info: Circuit breaker closed after successful recovery +{"successCount":0,"successThreshold":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.432Z info: Circuit breaker recorded success +{"state":"closed","successCount":0,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.436Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.439Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.445Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195223441} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:22.448Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.553Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.554Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.556Z error: Circuit breaker opened due to repeated failures +{"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195224556} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.558Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.564Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.567Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.571Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195224570} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.578Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.585Z info: Circuit breaker manually reset {"previousState":"closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.592Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.596Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.599Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.600Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.603Z error: Circuit breaker opened due to repeated failures +{"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792195224602} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.605Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":5,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:23.612Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/database-cleanup-job.test.ts + ● Console + + console.log + 2026-10-17T00:00:24.948Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.453Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.488Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.533Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/circuit-breaker.test.ts +PASS src/services/worker-manager.test.ts + ● Console + + console.log + 2026-10-17T00:00:23.995Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.015Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.020Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.024Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.040Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.046Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.048Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.053Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.054Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.057Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:24.064Z info: Initiating graceful shutdown {"activeJobs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.077Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.080Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.084Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.086Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.088Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.089Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.090Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.093Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.095Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.098Z warn: Graceful shutdown already initiated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.101Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-17T00:00:26.105Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.113Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts + ● Console + + console.log + 2026-10-17T00:00:25.207Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.230Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.232Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.234Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.538Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.540Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.542Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:25.543Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.305Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.307Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.310Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.314Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.711Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.714Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.716Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:26.717Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/store/event-registry.test.ts +PASS src/services/dead-letter-queue.test.ts + ● Console + + console.log + 2026-10-17T00:00:26.982Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.849Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.852Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.857Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.872Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.912Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.917Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-17T00:00:32.884Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.925Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.934Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.663Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.665Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.666Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.683Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.714Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"email","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.716Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-17T00:00:33.684Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.733Z info: Dead-letter notification requeued +{"requestId":"req-2","id":1,"notificationId":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.738Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/tests/notification-scheduler-refactored.test.ts + ● Console + + console.log + 2026-10-17T00:00:26.714Z info: Connected to SQLite database +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.496Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.499Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.504Z info: Database initialized successfully +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.548Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.607Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.629Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.650Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.688Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.702Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.740Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.761Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.800Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.847Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.863Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.894Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.917Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.951Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.981Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:27.984Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.016Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.030Z info: Notification cancelled {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.063Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.078Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.110Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.129Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.178Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.224Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.247Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.279Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.329Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.345Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.366Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.396Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.429Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.451Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.477Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.534Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.616Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.632Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.635Z info: Scheduling new notification +{"type":"email","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.652Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.656Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.670Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.674Z info: Scheduling new notification +{"type":"sms","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.693Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"sms"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.713Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.728Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:28.735Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/rate-limiter.test.ts +PASS src/services/notification-stats-cache.test.ts +PASS src/services/event-subscriber-checkpoint.test.ts +PASS src/services/rpc-rate-limiter.test.ts + ● Console + + console.log + 2026-10-17T00:00:29.857Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:29.937Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:29.990Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.054Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.111Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.289Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.303Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.319Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.345Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.373Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.405Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.421Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.436Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.452Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.479Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.507Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.539Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.554Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.570Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.587Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.603Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.621Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.637Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.652Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.669Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.694Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.711Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.730Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.757Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.774Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.788Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.807Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.825Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.841Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.857Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.873Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.904Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.922Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.947Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.965Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.979Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:30.995Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.019Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.035Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.052Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.068Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.086Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.108Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.134Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.154Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.196Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.232Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.249Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.267Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.282Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-metrics-store.test.ts +PASS src/services/notification-template-repository.test.ts + ● Console + + console.log + 2026-10-17T00:00:31.635Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.687Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.690Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.700Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.704Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.731Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.734Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.749Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.750Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.752Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.766Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.769Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.794Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.796Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.798Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.801Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.805Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.874Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.879Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.881Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.904Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.947Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.955Z info: Notification template updated +{"templateId":"tmpl-001","actor":"admin@example.com"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.962Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.965Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.980Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.982Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.984Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:31.987Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.018Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.023Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.033Z info: Template audit record created +{"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.035Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.038Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.041Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.055Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.056Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.060Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.067Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.070Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.074Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.103Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.104Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.109Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.130Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.134Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.137Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.150Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.152Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.154Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.157Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.165Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.168Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.183Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.185Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.186Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.189Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.194Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.203Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.206Z info: [TemplateCache] Template invalidated +{"templateId":"tmpl-001","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.207Z info: Notification template updated +{"templateId":"tmpl-001","actor":"cache-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.211Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.215Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.230Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.232Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.233Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.236Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.238Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.240Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.247Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET +actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.250Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.252Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.265Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.266Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.268Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.270Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.273Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.278Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.282Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log +WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.284Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.287Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.300Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.301Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.302Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.307Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.312Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.325Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.327Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.329Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:32.332Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/templates-api.test.ts +PASS src/tests/events-server.test.ts + ● Console + + console.log + 2026-10-17T00:00:33.168Z info: Handling GET /api/events +{"requestId":"a3eb68ff","correlationId":"7952e684-8dcf-4144-958d-327feae20269","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.189Z info: GET /api/events complete +{"requestId":"a3eb68ff","correlationId":"7952e684-8dcf-4144-958d-327feae20269","returned":0,"durationMs":22} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.202Z info: Handling GET /api/events +{"requestId":"d10c35dd","correlationId":"my-trace-abc123","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.204Z info: GET /api/events complete +{"requestId":"d10c35dd","correlationId":"my-trace-abc123","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.215Z info: Handling GET /api/events +{"requestId":"8d2a1979","correlationId":"4848654d-e7be-425d-8e00-a885804eef4d","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.218Z info: GET /api/events complete +{"requestId":"8d2a1979","correlationId":"4848654d-e7be-425d-8e00-a885804eef4d","returned":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.234Z warn: Unhandled request +{"requestId":"adabd184","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.239Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":6,"requestId":"adabd184"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.311Z warn: Unhandled request +{"requestId":"788fa567","correlationId":"97cbf836-3ef1-45ed-b4c0-69b2d713505f","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.314Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":4,"requestId":"788fa567"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.332Z warn: Unhandled request +{"requestId":"509a4bab","correlationId":"3548efae-96ec-4437-82b1-9f451c9647e5","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.335Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":4,"requestId":"509a4bab"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.353Z info: Handling GET /api/events +{"requestId":"960e7b8d","correlationId":"f46f7eff-d4f2-427c-8648-ff0068cd111c","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.355Z info: GET /api/events complete +{"requestId":"960e7b8d","correlationId":"f46f7eff-d4f2-427c-8648-ff0068cd111c","returned":20,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.365Z info: Handling GET /api/events +{"requestId":"841fa1d8","correlationId":"419b4c75-51ae-47ab-9f2c-c46c32c7b4aa","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.367Z info: GET /api/events complete +{"requestId":"841fa1d8","correlationId":"419b4c75-51ae-47ab-9f2c-c46c32c7b4aa","returned":50,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.376Z info: Handling GET /api/events +{"requestId":"3bd70fcc","correlationId":"252c6765-3653-4c42-8273-d3f4b8eced3f","limit":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.379Z info: GET /api/events complete +{"requestId":"3bd70fcc","correlationId":"252c6765-3653-4c42-8273-d3f4b8eced3f","returned":100,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.386Z info: Handling GET /api/events +{"requestId":"c2840f79","correlationId":"f30edd01-4231-45a4-85e3-0e5356fddb88","limit":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.388Z info: GET /api/events complete +{"requestId":"c2840f79","correlationId":"f30edd01-4231-45a4-85e3-0e5356fddb88","returned":1,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.397Z info: Handling GET /api/events +{"requestId":"e6f64b6f","correlationId":"7f75e0a7-88e1-421a-858f-e5c8250d7c49","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.400Z info: GET /api/events complete +{"requestId":"e6f64b6f","correlationId":"7f75e0a7-88e1-421a-858f-e5c8250d7c49","returned":20,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load.test.ts + ● Console + + console.log + + === Load Test Results (100 events) === + + at Object. (src/__tests__/load.test.ts:62:15) + + console.log + Total processing time: 256ms + + at Object. (src/__tests__/load.test.ts:63:15) + + console.log + Throughput: 390.63 events/second + + at Object. (src/__tests__/load.test.ts:64:15) + + console.log + Average latency per event: 2.56ms + + at Object. (src/__tests__/load.test.ts:65:15) + + console.log + + === Load Test Results (500 events, maxConcurrency=20) === + + at Object. (src/__tests__/load.test.ts:109:15) + + console.log + Total processing time: 489ms + + at Object. (src/__tests__/load.test.ts:110:15) + + console.log + Throughput: 1022.49 events/second + + at Object. (src/__tests__/load.test.ts:111:15) + + console.log + Average latency per event: 0.98ms + + at Object. (src/__tests__/load.test.ts:112:15) + +PASS src/migrations/005-notification-expiration.test.ts + ● Console + + console.log + 2026-10-17T00:00:33.932Z info: Migration 005 (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.033Z info: Migration 005-repeat (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.057Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.073Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-service.test.ts + ● Console + + console.log + 2026-10-17T00:00:33.867Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.890Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.895Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.901Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.906Z info: Notification template created {"templateId":"svc-template"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.917Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.931Z info: Template audit record created +{"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.936Z info: [TemplateCache] Template invalidated +{"templateId":"svc-template","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.944Z info: Notification template updated +{"templateId":"svc-template","actor":"service-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.950Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.962Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.966Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.985Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.988Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.990Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.997Z info: Notification template created {"templateId":"tmpl-a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:33.998Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.000Z info: Notification template created {"templateId":"tmpl-b"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.002Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.005Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.009Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.021Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.022Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.024Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.028Z info: Notification template created {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.030Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.034Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.036Z info: Notification template deleted {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.037Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.040Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.046Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.057Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.060Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.062Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.064Z info: Notification template created {"templateId":"render-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.066Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.068Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.071Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.081Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.082Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.084Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.086Z info: Notification template created {"templateId":"render-missing"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.088Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.094Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.097Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.108Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.111Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.113Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.115Z info: Notification template created {"templateId":"no-vars"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.116Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:34.118Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/content-type.test.ts +PASS src/services/payload-validation.integration.test.ts +PASS src/services/retry-policy.test.ts +PASS src/api/events-server.history.test.ts +PASS src/__tests__/api-contract.test.ts + ● Console + + console.log + 2026-10-17T00:00:35.698Z info: Handling GET /api/events +{"requestId":"df60189b","correlationId":"7a7c85dd-3f24-43a9-824b-e1eaa8dc7e16","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.716Z info: GET /api/events complete +{"requestId":"df60189b","correlationId":"7a7c85dd-3f24-43a9-824b-e1eaa8dc7e16","returned":0,"durationMs":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.753Z warn: Unhandled request {"requestId":"0ed0dd50","correlationId":"35c12af3-dcb5 +-432f-9ac4-e66e29a91475","method":"GET","url":"/api/definitely-not-a-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.757Z info: Request completed +{"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":4,"requestId":"0ed0dd50"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.771Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.783Z info: Handling GET /api/analytics {"requestId":"78e67ff4","correlationId":"30c +3d47b-f9bf-4c59-8ba8-ab8e49ad3f10","totalRecorded":0,"reset":false,"durationMs":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.790Z info: Handling GET /api/events +{"requestId":"6619b6c7","correlationId":"6cc2e4f1-d141-4011-8e97-193f6a43a4d2","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:35.794Z info: GET /api/events complete +{"requestId":"6619b6c7","correlationId":"6cc2e4f1-d141-4011-8e97-193f6a43a4d2","returned":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/config/schema-validator.test.ts +ts-jest[ts-compiler] (WARN) src/api/__tests__/api-contract.test.ts:20:46 - +error TS2345: Argument of type '{ status: string; }' is not assignable to parameter of type 'never'. + +20 getHealth: jest.fn().mockResolvedValue({ status: 'healthy' }), +   ~~~~~~~~~~~~~~~~~~~~~ +src/api/__tests__/api-contract.test.ts:60:38 - error TS2345: Argument of +type '{ ok: number; }' is not assignable to parameter of type 'never'. + +60 get: jest.fn().mockResolvedValue({ ok: 1 }), +   ~~~~~~~~~ +PASS src/api/__tests__/api-contract.test.ts +PASS src/services/notification-analytics-aggregator.test.ts +PASS src/api/events-server.health.test.ts +ts-jest[ts-compiler] (WARN) src/services/providers/provider-health.test.ts:15:56 - +error TS2345: Argument of type '{}' is not assignable to parameter of type 'DiscordConfig'. + Type '{}' is missing the following properties from type 'DiscordConfig': webhookUrl, webhookId + +15 const provider = new DiscordNotificationProvider({}); +   ~~ +src/services/providers/provider-health.test.ts:26:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +26 const provider = new DiscordNotificationProvider({ +   ~ +27 webhookUrl: 'https://discord.com/api/webhooks/123/my-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +28 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +src/services/providers/provider-health.test.ts:42:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +42 const provider = new DiscordNotificationProvider({ +   ~ +43 webhookUrl: 'https://discord.com/api/webhooks/123456789/my-super-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +44 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +PASS src/services/providers/provider-health.test.ts + ● Console + + console.log + 2026-10-17T00:00:37.567Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-deduplication-service.test.ts +PASS src/api/rate-limit-scenarios.test.ts +PASS src/migrations/004-database-data-integrity.test.ts + ● Console + + console.log + 2026-10-17T00:00:38.435Z info: Migration 004 (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.539Z info: Migration 004-repeat (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.602Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 +aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 +row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data +first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.616Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.629Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.631Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.632Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.636Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', +'2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.638Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/redact.test.ts +PASS src/config-logging.test.ts +PASS src/services/scheduled-notification-repository-cache.test.ts + ● Console + + console.log + 2026-10-17T00:00:38.857Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.874Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.880Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.884Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.904Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.914Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.918Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.924Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.928Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.930Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.932Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.934Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.936Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.938Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.940Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.942Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.944Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + Performance comparison: + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:130:15) + + console.log + Uncached call: 2ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:131:15) + + console.log + Cached call: 0ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:132:15) + + console.log + Improvement: 100.0% + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:133:15) + + console.log + 2026-10-17T00:00:38.951Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.954Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.956Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.958Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.963Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.969Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.973Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.976Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:38.990Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/backpressure-controller.test.ts + ● Console + + console.log + 2026-10-17T00:00:39.040Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.063Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.066Z info: Backpressure deactivated: queue recovered +{"queueSize":49,"threshold":50,"durationMs":3,"targetThroughputPerSec":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.070Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.076Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.081Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.086Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.090Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.096Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/logger.test.ts +PASS src/api/notifications-history.test.ts +PASS src/api/delivery-metrics-api.test.ts + ● Console + + console.log + 2026-10-17T00:00:39.925Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"b50b01b6" +,"correlationId":"2ddaae81-eebe-4f98-ba05-b24359ada7f6","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.975Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"b171b2bb" +,"correlationId":"dc5480f1-29e8-431b-92e0-68491f1e5679","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.989Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"8bafe7ae" +,"correlationId":"0976c5b9-d8db-4fb6-8f0f-71980278c744","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs" +:0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:39.991Z info: Delivery metrics window reset after read +{"requestId":"8bafe7ae","correlationId":"0976c5b9-d8db-4fb6-8f0f-71980278c744"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.003Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"62aded42" +,"correlationId":"667261f7-e51e-4fab-b31e-c0daddd4c8ed","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.017Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"aed9620c" +,"correlationId":"2ea7a628-18fd-412d-b931-daa0dc624368","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.030Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"a8a334c2","correlationId":"f9128dc0-a388-4f40-9b9f-edbb384c4edb","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.033Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"a8a334c2","correlationId":"f9128dc0-a388-4f40-9b9f-edbb384c4edb","count":1,"durationMs":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.053Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"7a9e8b6c","correlationId":"161007c7-9caf-4ef9-bf0c-b8138b6d47a0","limit":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.055Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"7a9e8b6c","correlationId":"161007c7-9caf-4ef9-bf0c-b8138b6d47a0","count":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.068Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"3 +7852937","correlationId":"ecfee6f9-cb54-45eb-b239-aa7f74bd6726","limit":50,"since":"2024-01-01T00:00:00.000Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.071Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"37852937","correlationId":"ecfee6f9-cb54-45eb-b239-aa7f74bd6726","count":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.090Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"3114adc7","correlationId":"e15ee8a5-426e-4860-bf4e-2a082489d534","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.093Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"3114adc7","correlationId":"e15ee8a5-426e-4860-bf4e-2a082489d534","count":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/stellar-rpc-manager.test.ts +PASS src/scripts/replay-event.test.ts + ● Console + + console.log + 2026-10-17T00:00:40.295Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.311Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.314Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.317Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.321Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.329Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.340Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.344Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.346Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.348Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.350Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.363Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.365Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.367Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.374Z info: Event processed and recorded {"eventId":"duplicate-test-event","contract +Address":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a0 +26ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.383Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.386Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.399Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.400Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.401Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.417Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAd +dress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de +1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.419Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.422Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.436Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.438Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.440Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.449Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.452Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.465Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.467Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.468Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.471Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.474Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.487Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.489Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.491Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.495Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-scheduler.test.ts + ● Console + + console.log + 2026-10-17T00:00:40.539Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.555Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.557Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.559Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.566Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.568Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.569Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.571Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.578Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.583Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.588Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.598Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.602Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/payload-compression.test.ts + ● Console + + console.log + 2026-10-17T00:00:40.748Z info: Payload compressed +{"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.783Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:40.786Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/app-version.test.ts +PASS src/services/event-provenance.test.ts + ● Console + + console.log + 2026-10-17T00:00:41.137Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.166Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.174Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.190Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.215Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/middleware/body-limit.test.ts +PASS src/services/template-service.test.ts + ● Console + + console.log + 2026-10-17T00:00:41.451Z info: Template created successfully +{"templateId":1,"uniqueKey":"welcome-email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.496Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome +{{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.501Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" +","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.505Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.511Z error: Failed to create template {"error":{"message":"description: must be a +string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","description":12345}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.516Z error: Failed to create template {"error":{"message":"name: must not exceed +255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTe +mplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.520Z error: Failed to create template {"error":{"message":"variables: must be an +array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","variables":[1,2,3]}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.525Z error: Failed to create template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","defaultValues":"nope"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.536Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.541Z error: Failed to update template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.550Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.554Z error: Failed to update template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-processing-queue.test.ts +PASS src/schema/compatibility-check.test.ts +PASS src/services/notification-api.test.ts + ● Console + + console.log + 2026-10-17T00:00:41.845Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.886Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.888Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.905Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.916Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:41.933Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-cache.test.ts + ● Console + + console.log + 2026-10-17T00:00:42.151Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.166Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.168Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.171Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.174Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.177Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.180Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.182Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.184Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.186Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.189Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.191Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.196Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.199Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.201Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.203Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.207Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.209Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.213Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.215Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.218Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.221Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.223Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.226Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.229Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.233Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.236Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.239Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +ts-jest[ts-compiler] (WARN) src/services/event-normalization.test.ts:101:35 - +error TS2353: Object literal may only specify known properties, and 'payload_version' does not exist +in type 'ScMapEntry[]'. + +101 value: xdr.ScVal.scvMap({ payload_version: 1, notification_id: 'abc' }), +   ~~~~~~~~~~~~~~~ +PASS src/services/event-normalization.test.ts +PASS src/services/notification-health-monitor.test.ts + ● Console + + console.log + 2026-10-17T00:00:42.496Z info: NotificationHealthMonitor started {"intervalMs":30000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.519Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-store-consistency.test.ts + ● Console + + console.log + 2026-10-17T00:00:42.624Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.645Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.648Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.653Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:42.684Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/shutdown-unit.test.ts +PASS src/api/api-version-registry.test.ts +PASS src/__tests__/load-test-runner.test.ts +PASS src/services/historical-backfill-service.test.ts +PASS src/services/dns-rebinding-guard.test.ts +PASS src/utils/logger-structured.test.ts +PASS src/utils/ledger-range-validator.test.ts +PASS src/services/subscription-event-emitter.test.ts +PASS src/services/provider-registry.test.ts + ● Console + + console.log + 2026-10-17T00:00:43.308Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock +Provider","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:43.331Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing +Mock","capabilities":[]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:43.335Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial +Mock","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/test-utils/notification-fixture-builder.test.ts +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/utils/notification-version.test.ts +PASS src/utils/event-utils.test.ts +PASS src/services/idempotency-key-service.test.ts +PASS src/api/cors-validator.test.ts +PASS src/utils/cors-validator.test.ts +PASS src/__tests__/integration.test.ts + ● Console + + console.log + 2026-10-17T00:00:43.852Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":2,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/search-suggestion.test.ts + ● Console + + console.log + 2026-10-17T00:00:43.816Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:43.836Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:43.839Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:43.847Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:43.933Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/log-level-resolver.test.ts +PASS src/utils/metadata-validator.test.ts +PASS src/providers/provider-capabilities.test.ts +PASS src/store/preference-store.test.ts +PASS src/middleware/request-id.test.ts +PASS src/services/notification-search-service.test.ts + ● Console + + console.log + 2026-10-17T00:00:44.333Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:44.353Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:44.357Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:44.363Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:44.367Z info: Notification search complete +{"total":1,"returned":1,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/subscription-service.test.ts +PASS src/middleware/security-headers.test.ts +PASS src/utils/request-id.test.ts +PASS src/utils/payload-size-validator.test.ts +PASS src/utils/validation.test.ts +PASS src/utils/batch-validator.test.ts +PASS src/config-schema.test.ts +PASS src/utils/pagination.test.ts +PASS src/services/notification-metrics-runner.test.ts +PASS src/services/delivery-receipt-repository.test.ts +PASS src/services/dead-letter-manager.test.ts +PASS src/api/request-size-limiter.test.ts +PASS src/services/event-quarantine.test.ts +PASS src/examples/webhook-consumer-verification-example.test.ts +PASS src/services/retry-statistics.test.ts +PASS src/services/job-monitor.test.ts + ● Console + + console.log + 2026-10-17T00:00:46.116Z error: Job monitor: job failed +{"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/json-logger-formatter.test.ts +PASS src/services/webhook-replay-cache.test.ts +PASS src/services/notification-import-service.test.ts + ● Console + + console.log + 2026-10-17T00:00:46.409Z info: Bulk notification import complete +{"imported":2,"skipped":0,"total":2,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:46.431Z info: Bulk notification import complete +{"imported":1,"skipped":2,"total":3,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-17T00:00:46.436Z info: Bulk notification import complete +{"imported":1,"skipped":1,"total":2,"format":"csv"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/config-snapshot.test.ts +PASS src/services/indexing-reconciliation-engine.test.ts +A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due +to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure +that .unref() was called on them. + +Summary of all failing tests +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (64.658 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   169 | +  170 | describe('fan-out delivery', () => { + > 171 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  172 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  173 | +  174 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:171:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   178 | }); +  179 | + > 180 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  181 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  182 | +  183 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:180:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   194 | +  195 | describe('channel-specific payloads', () => { + > 196 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  197 | await deliverToAllChannels( +  198 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  199 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:196:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   210 | }); +  211 | + > 212 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  213 | await deliverToAllChannels( +  214 | createMockEvent({ id: 'event-system', type: +'system' }) +  215 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:212:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (74.57 s) + ● Notification delivery lifecycle (e2e) › happy path delivery › delivers a contract event to Discord and marks +fingerprint as sent + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   93 | // ========================================================================= +  94 | describe('happy path delivery', () => { + > 95 | it('delivers a contract event to Discord and marks fingerprint as +sent', async () => { +  | ^ +  96 | const service = new +DiscordNotificationService(discordConfig); +  97 | const result = await +service.sendEventNotification(makeEvent(), contractCfg, 'req-happy'); +  98 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:95:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › includes ledger number in the embed + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   114 | }); +  115 | + > 116 | it('includes ledger number in the embed', +async () => { +  | ^ +  117 | const service = new +DiscordNotificationService(discordConfig); +  118 | await service.sendEventNotification( +  119 | makeEvent({ txHash: 'tx-abc123', ledger: +42000 }), + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:116:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › returns true and does not call fetch for duplicate +events (deduplication) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   131 | }); +  132 | + > 133 | it('returns true and does not call fetch for duplicate events +(deduplication)', async () => { +  | ^ +  134 | const service = new +DiscordNotificationService(discordConfig); +  135 | const event = makeEvent({ id: 'evt-dedup-1' +}); +  136 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:133:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders contract events with color 0xff00 + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders system events with color 0x99ff + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles multi-symbol topic arrays + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   253 | // ========================================================================= +  254 | describe('event topic rendering', () => { + > 255 | it('handles multi-symbol topic arrays', +async () => { +  | ^ +  256 | const service = new +DiscordNotificationService(discordConfig); +  257 | const event = makeEvent({ +  258 | topic: +[xdr.ScVal.scvSymbol('task'), +xdr.ScVal.scvSymbol('completed')], + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:255:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:254:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   284 | // ========================================================================= +  285 | describe('network errors', () => { + > 286 | it('returns false and logs error when fetch +throws', async () => { +  | ^ +  287 | fetchMock.mockRejectedValueOnce(new +Error('ECONNRESET')); +  288 | +  289 | const service = new +DiscordNotificationService(discordConfig); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:286:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   304 | }); +  305 | + > 306 | it('returns false on timeout (abort)', +async () => { +  | ^ +  307 | // Simulate a slow response that gets aborted +  308 | fetchMock.mockImplementationOnce( +  309 | () => + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:306:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › allows same event after dedup window expires + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   330 | // ========================================================================= +  331 | describe('deduplication', () => { + > 332 | it('allows same event after dedup window +expires', async () => { +  | ^ +  333 | const dedup = new NotificationDeduplicator({ +  334 | windowMs: 100, +  335 | maxSize: 100, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:332:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › evicts oldest entry when cache is at capacity + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   355 | }); +  356 | + > 357 | it('evicts oldest entry when cache is at +capacity', async () => { +  | ^ +  358 | const dedup = new NotificationDeduplicator({ +maxSize: 2, windowMs: 60000 }); +  359 | const service = new +DiscordNotificationService(discordConfig, dedup); +  360 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:357:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › delivery report › DiscordNotificationService tracks metrics via +deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   409 | // ========================================================================= +  410 | describe('delivery report', () => { + > 411 | it('DiscordNotificationService tracks metrics via +deduplicator', async () => { +  | ^ +  412 | const service = new +DiscordNotificationService(discordConfig); +  413 | +  414 | await service.sendEventNotification(makeEvent({ id: +'evt-metrics-1' }), contractCfg); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:411:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:410:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (19.279 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/services/retry-deduplication.test.ts (14.618 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/services/discord-notification-refactored.test.ts (90.246 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   243 | const result = await +service.sendTestMessage(); +  244 | + > 245 | expect(result).toBe(true); +  | ^ +  246 | }); +  247 | +  248 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification-refactored.test.ts:245:22) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/services/scheduled-notification-queue.test.ts (8.77 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/services/concurrent-event-processing.test.ts (18.443 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/execution-metrics.test.ts (12.653 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/api/execution-metrics-api.test.ts (11.5 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/api/queue-operational-metrics.test.ts (23.04 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +FAIL src/tests/payload-integrity.test.ts + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/api/events-server.test.ts (6.301 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/tests/template-api-integration.test.ts (5.565 s) + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/retry-policy-integration.test.ts (6.544 s) + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/__tests__/stress.test.ts (297.53 s) + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 950 + Received: 900 + +   712 | console.log(` Final Success Rate: ${((processedIds.length / +eventCount) * 100).toFixed(2)}%`); +  713 | + > 714 | +expect(processedIds.length).toBeGreaterThanOrEqual(eventCount * +0.95); // At least 95% success +  | ^ +  715 | expect(retryCount).toBeGreaterThan(0); // Verify +retries occurred +  716 | }, 180000); +  717 | }); + + at Object. (src/__tests__/stress.test.ts:714:35) + +FAIL src/services/notification-failure-scenarios.test.ts (183.347 s) + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › tracks timeoutCount in DiscordNotificationService on +AbortError + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 1 + Received: 0 + +   71 | const success = await +service.sendTestMessage(); +  72 | expect(success).toBe(false); + > 73 | +expect(service.getMetrics().timeoutCount).toBeGreaterThanOrEqual(1); +  | ^ +  74 | }); +  75 | }); +  76 | + + at Object. (src/services/notification-failure-scenarios.test.ts:73:49) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/services/webhook-retry-helper.test.ts (194.791 s) + ● sendWebhookWithRetry › network/connection errors are retried › succeeds after a network error is resolved on retry + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   195 | }); +  196 | + > 197 | it('succeeds after a network error is resolved on +retry', async () => { +  | ^ +  198 | mockSendWebhook +  199 | .mockRejectedValueOnce(makeNetworkError()) +  200 | .mockResolvedValueOnce(makeResponse(200)); + + at src/services/webhook-retry-helper.test.ts:197:7 + at src/services/webhook-retry-helper.test.ts:184:11 + at Object. (src/services/webhook-retry-helper.test.ts:61:9) + + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/tests/template-system.test.ts (5.587 s) + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/services/webhook-delivery-service.test.ts (26.9 s) + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 1, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/webhook-verifier.test.ts (28.135 s) + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +FAIL src/tests/api-versioning.test.ts (25.583 s) + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/services/event-subscriber-backfill.test.ts (8.01 s) + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/services/retry-scheduler-webhook.test.ts (6.595 s) + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +FAIL src/database/database-transaction.test.ts (6.343 s) + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/api/template-routes.test.ts (10.114 s) + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts (11.368 s) + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/services/archive.test.ts (7.916 s) + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/services/event-subscriber.test.ts (16.035 s) + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792108790187} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792195191738, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792108790187} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792195191738, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "f363ab27"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/services/event-subscriber-reorg.test.ts (6.879 s) + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "537ee96d"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "7603e27b-8fdf-4bf6-9238-3f2eb407c5ee", +"eventId": "event-1", "requestId": "7603e27b-8fdf-4bf6-9238-3f2eb407c5ee"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/services/notification-retry-queue.test.ts (5.007 s) + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4660.864163126824, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:59:58.339Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/__tests__/retry-scheduler.integration.test.ts (9.224 s) + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/providers/provider-capabilities.test.ts (7.072 s) + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/__tests__/config-startup-validation.test.ts (6.347 s) + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/api/request-duration-logger.test.ts + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4226.063998797194, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-17T00:00:07.168Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/discord-notification.test.ts (13.863 s) + ● DiscordNotificationService › sendEventNotification › should send event notification successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   58 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  59 | + > 60 | expect(result).toBe(true); +  | ^ +  61 | expect(mockFetch).toHaveBeenCalledTimes(1); +  62 | const [url, options] = +mockFetch.mock.calls[0]; +  63 | expect(url).toBe(mockConfig.webhookUrl); + + at Object. (src/services/discord-notification.test.ts:60:22) + + ● DiscordNotificationService › sendEventNotification › should handle request timeout + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Discord webhook request timed out", ObjectContaining {"timeoutMs": 100, "webhookId": "123"} + Received + 1: "Discord webhook request error", {"attempt": 0, "contractAddress": "CA123", "correlationId": +"d0da9553-273a-42ad-acf7-bd7ce982e61c", "durationMs": 101, "error": [DnsRebindingError: DNS resolution returned no +addresses for hostname: discord.com], "eventId": "event-123", "requestId": "d0da9553-273a-42ad-acf7-bd7ce982e61c", +"webhookId": "123"} + 2: "Exceeded max Discord retry attempts", {"contractAddress": "CA123", "correlationId": +"d0da9553-273a-42ad-acf7-bd7ce982e61c", "eventId": "event-123", "maxRetries": 0, "requestId": +"d0da9553-273a-42ad-acf7-bd7ce982e61c", "webhookId": "123"} + + Number of calls: 2 + +   111 | +  112 | expect(result).toBe(false); + > 113 | expect(mockLoggerModule.error).toHaveBeenCalledWith( +  | ^ +  114 | 'Discord webhook request timed out', +  115 | expect.objectContaining({ +  116 | webhookId: mockConfig.webhookId, + + at Object. (src/services/discord-notification.test.ts:113:38) + + ● DiscordNotificationService › duplicate detection › skips the webhook call for a duplicate event + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   133 | const secondResult = await +service.sendEventNotification(mockEvent, mockContractConfig); +  134 | + > 135 | +expect(mockFetch).toHaveBeenCalledTimes(1); +  | ^ +  136 | expect(secondResult).toBe(true); +  137 | expect(service.getDeduplicationMetrics()).toEqual( +  138 | expect.objectContaining({ skippedDuplicates: 1, +cacheSize: 1 }), + + at Object. (src/services/discord-notification.test.ts:135:25) + + ● DiscordNotificationService › duplicate detection › logs a duplicate detection event + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping duplicate notification", ObjectContaining {"contractAddress": "CA123", "eventId": +"event-dup-log"} + Received + 1: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"3de85a7a-ecfb-4a51-8421-5e9cb1e3980a", "eventId": "event-dup-log", "requestId": +"3de85a7a-ecfb-4a51-8421-5e9cb1e3980a", "webhookId": "123"} + 2: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"c1d6d148-ca0e-4ff3-b9cf-58da3bdd693f", "eventId": "event-dup-log", "requestId": +"c1d6d148-ca0e-4ff3-b9cf-58da3bdd693f", "webhookId": "123"} + + Number of calls: 2 + +   151 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  152 | + > 153 | expect(mockLoggerModule.info).toHaveBeenCalledWith( +  | ^ +  154 | 'Skipping duplicate notification', +  155 | expect.objectContaining({ +  156 | eventId: 'event-dup-log', + + at Object. (src/services/discord-notification.test.ts:153:37) + + ● DiscordNotificationService › duplicate detection › allows the same notification request after the configured +window expires + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   173 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  174 | + > 175 | expect(result).toBe(true); +  | ^ +  176 | expect(mockFetch).toHaveBeenCalledTimes(2); +  177 | expect(service.getDeduplicationMetrics()).toEqual( +  178 | expect.objectContaining({ acceptedRequests: 2, +skippedDuplicates: 1 }), + + at Object. (src/services/discord-notification.test.ts:175:22) + + ● DiscordNotificationService › duplicate detection › allows the same event id on a different contract through + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 0 + +   191 | const result = await +service.sendEventNotification(mockEvent, contractB); +  192 | + > 193 | +expect(mockFetch).toHaveBeenCalledTimes(2); +  | ^ +  194 | expect(result).toBe(true); +  195 | }); +  196 | + + at Object. (src/services/discord-notification.test.ts:193:25) + + ● DiscordNotificationService › duplicate detection › does not mark an event as sent when the webhook call fails + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   213 | +  214 | expect(firstResult).toBe(false); + > 215 | expect(secondResult).toBe(true); +  | ^ +  216 | expect(mockFetch).toHaveBeenCalledTimes(2); +  217 | }); +  218 | + + at Object. (src/services/discord-notification.test.ts:215:28) + + ● DiscordNotificationService › duplicate detection › accepts an injected deduplicator + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   227 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  228 | + > 229 | +expect(deduplicator.size()).toBe(1); +  | ^ +  230 | +expect(deduplicator.isDuplicate(generateFingerprint('event-injected', +'CA123'))).toBe(true); +  231 | }); +  232 | }); + + at Object. (src/services/discord-notification.test.ts:229:35) + + ● DiscordNotificationService › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   239 | const result = await +service.sendTestMessage(); +  240 | + > 241 | expect(result).toBe(true); +  | ^ +  242 | }); +  243 | +  244 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification.test.ts:241:22) + + ● DiscordNotificationService › formatEventMessage › should format event with string value correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   264 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  265 | + > 266 | expect(result).toBe(true); +  | ^ +  267 | const [, options] = +mockFetch.mock.calls[0]; +  268 | const body = +JSON.parse(options.body); +  269 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:266:22) + + ● DiscordNotificationService › formatEventMessage › should truncate long string values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   283 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  284 | + > 285 | expect(result).toBe(true); +  | ^ +  286 | const [, options] = +mockFetch.mock.calls[0]; +  287 | const body = +JSON.parse(options.body); +  288 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:285:22) + + ● DiscordNotificationService › formatEventMessage › should handle symbol type values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   302 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  303 | + > 304 | expect(result).toBe(true); +  | ^ +  305 | const [, options] = +mockFetch.mock.calls[0]; +  306 | const body = +JSON.parse(options.body); +  307 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:304:22) + + ● DiscordNotificationService › formatEventMessage › should preserve short messages without truncation + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   320 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  321 | + > 322 | expect(result).toBe(true); +  | ^ +  323 | const [, options] = +mockFetch.mock.calls[0]; +  324 | const body = +JSON.parse(options.body); +  325 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:322:22) + + ● DiscordNotificationService › formatEventMessage › should truncate oversized embed titles + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   340 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  341 | + > 342 | expect(result).toBe(true); +  | ^ +  343 | const [, options] = +mockFetch.mock.calls[0]; +  344 | const body = +JSON.parse(options.body); +  345 | expect(body.embeds[0].title.length).toBeL +essThanOrEqual(256); + + at Object. (src/services/discord-notification.test.ts:342:22) + + ● DiscordNotificationService › formatEventMessage › should enforce total embed size limit + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   361 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  362 | + > 363 | expect(result).toBe(true); +  | ^ +  364 | const [, options] = +mockFetch.mock.calls[0]; +  365 | const body = +JSON.parse(options.body); +  366 | const embed = body.embeds[0]; + + at Object. (src/services/discord-notification.test.ts:363:22) + + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   380 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  381 | + > 382 | expect(result).toBe(true); +  | ^ +  383 | const [, options] = +mockFetch.mock.calls[0]; +  384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:382:22) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone +embedded in an scvString payload + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   459 | +  460 | describe('integration: mention in event string value reaches embed +sanitized', () => { + > 461 | it('strips @everyone embedded in an scvString +payload', async () => { +  | ^ +  462 | mockFetch.mockResolvedValueOnce({ ok: true }); +  463 | +  464 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:461:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone in +event topic symbol used as embed title + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   487 | }); +  488 | + > 489 | it('strips @everyone in event topic symbol used as embed +title', async () => { +  | ^ +  490 | mockFetch.mockResolvedValueOnce({ ok: true }); +  491 | +  492 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:489:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + +Test Suites: 46 failed, 1 skipped, 105 passed, 151 of 152 total +Tests: 283 failed, 4 skipped, 2265 passed, 2552 total +Snapshots: 0 total +Time: 406.826 s +Ran all test suites. diff --git a/listener/_jest-run.txt b/listener/_jest-run.txt new file mode 100644 index 00000000..bc9b94e6 --- /dev/null +++ b/listener/_jest-run.txt @@ -0,0 +1,25060 @@ + +> listener@1.0.0 test +> node ./node_modules/jest/bin/jest.js + +node.exe : FAIL src/__tests__/scheduler-concurrency.test.ts (50.234 s) +At line:1 char:1 ++ & "C:\Program Files\nodejs/node.exe" "C:\Program Files\nodejs/node_mo ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/__test...t.ts (50.234 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ● Console + + console.log + 2026-10-16T23:28:31.639Z info: Connected to SQLite database +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.315Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.384Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.399Z info: Database initialized successfully +{"path":"./data/test-scheduler-concurrency.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.472Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.752Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.784Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.840Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.850Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.883Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.894Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.938Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:36.326Z info: Fetched and locked pending notifications +{"requestId":"1350bfd6","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:36.345Z info: Processing batch of scheduled notifications +{"requestId":"1350bfd6","count":4,"processorId":"test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:36.352Z info: Processing scheduled notification +{"requestId":"1350bfd6","id":1,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:36.497Z info: Notification marked as completed {"requestId":"1350bfd6","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:36.590Z info: Notification delivered successfully +{"requestId":"1350bfd6","id":1,"type":"discord","durationMs":107} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:36.775Z info: Processing scheduled notification +{"requestId":"1350bfd6","id":2,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:37.023Z info: Notification marked as completed {"requestId":"1350bfd6","id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:37.221Z info: Notification delivered successfully +{"requestId":"1350bfd6","id":2,"type":"discord","durationMs":169} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:37.225Z info: Processing scheduled notification +{"requestId":"1350bfd6","id":3,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:37.340Z info: Notification marked as completed {"requestId":"1350bfd6","id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:37.873Z info: Notification delivered successfully +{"requestId":"1350bfd6","id":3,"type":"discord","durationMs":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:37.889Z info: Processing scheduled notification +{"requestId":"1350bfd6","id":4,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:38.176Z info: Notification marked as completed {"requestId":"1350bfd6","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:38.304Z info: Notification delivered successfully +{"requestId":"1350bfd6","id":4,"type":"discord","durationMs":224} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:38.313Z info: Scheduler batch complete +{"requestId":"1350bfd6","processorId":"test-uuid-0001","count":4,"durationMs":2009} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.500Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.710Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.870Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.906Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.957Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.007Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.094Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.124Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.227Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.604Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.038Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.125Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.227Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.024Z info: Fetched and locked pending notifications +{"requestId":"3f102d37","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.107Z info: Processing batch of scheduled notifications +{"requestId":"3f102d37","count":6,"processorId":"test-uuid-0002"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.118Z info: Processing scheduled notification +{"requestId":"3f102d37","id":5,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:42.124Z info: Processing scheduled notification +{"requestId":"3f102d37","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:42.132Z info: Processing scheduled notification +{"requestId":"3f102d37","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:43.551Z info: Notification marked as completed {"requestId":"3f102d37","id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:43.628Z info: Notification marked as completed {"requestId":"3f102d37","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:43.680Z info: Notification marked as completed {"requestId":"3f102d37","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:43.740Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:43.977Z info: Notification delivered successfully +{"requestId":"3f102d37","id":6,"type":"discord","durationMs":244} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:44.236Z info: Processing scheduled notification +{"requestId":"3f102d37","id":8,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:44.309Z info: Notification delivered successfully +{"requestId":"3f102d37","id":5,"type":"discord","durationMs":244} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:44.357Z info: Processing scheduled notification +{"requestId":"3f102d37","id":9,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:44.473Z info: Notification delivered successfully +{"requestId":"3f102d37","id":7,"type":"discord","durationMs":242} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:44.653Z info: Processing scheduled notification +{"requestId":"3f102d37","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:44.750Z info: Notification marked as completed {"requestId":"3f102d37","id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:44.758Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.803Z info: Notification marked as completed {"requestId":"3f102d37","id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:44.884Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n +","params":[8,1,"SUCCESS",null,null,489],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.957Z error: Failed to process notification +{"requestId":"3f102d37","id":8,"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}","attempt":1,"durationMs":722} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:44.964Z error: Job monitor: job failed +{"jobId":"notification-8","jobName":"scheduled-notification","error":"SQLITE_CONSTRAINT: FOREIGN KEY constraint +failed","durationMs":739} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:44.987Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.992Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.001Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n +","params":[9,1,"SUCCESS",null,null,365],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.005Z error: Failed to process notification +{"requestId":"3f102d37","id":9,"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}","attempt":1,"durationMs":648} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:45.035Z error: Job monitor: job failed +{"jobId":"notification-9","jobName":"scheduled-notification","error":"SQLITE_CONSTRAINT: FOREIGN KEY constraint +failed","durationMs":695} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:45.140Z info: Notification marked as completed {"requestId":"3f102d37","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:45.175Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.189Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n +","params":[10,1,"SUCCESS",null,null,183],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.199Z error: Failed to process notification +{"requestId":"3f102d37","id":10,"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}","attempt":1,"durationMs":545} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:45.220Z error: Job monitor: job failed +{"jobId":"notification-10","jobName":"scheduled-notification","error":"SQLITE_CONSTRAINT: FOREIGN KEY constraint +failed","durationMs":568} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:45.367Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:45.374Z info: Notification marked for retry or failed +{"id":9,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:45.397Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[8,1,"FAILED","SQLITE_CONSTRAINT: +FOREIGN KEY constraint failed",null,722],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.401Z error: Error in scheduler processing loop {"requestId":"3f102d37","error":"{\" +errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}","processorId":"test-uuid-0002","durationMs":3692} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.435Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[9,1,"FAILED","SQLITE_CONSTRAINT: +FOREIGN KEY constraint failed",null,648],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.444Z info: Notification marked for retry or failed +{"id":10,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 2) + + console.log + 2026-10-16T23:28:45.605Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[10,1,"FAILED","SQLITE_CONSTRAINT: +FOREIGN KEY constraint failed",null,545],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.601Z info: Fetched and locked pending notifications +{"requestId":"8eabae79","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.605Z info: Processing batch of scheduled notifications +{"requestId":"8eabae79","count":2,"processorId":"test-uuid-0003"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.609Z info: Processing scheduled notification +{"requestId":"8eabae79","id":11,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:46.620Z info: Processing scheduled notification +{"requestId":"8eabae79","id":12,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:46.998Z info: Notification marked as completed {"requestId":"8eabae79","id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:47.006Z info: Notification marked as completed {"requestId":"8eabae79","id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:47.102Z info: Notification delivered successfully +{"requestId":"8eabae79","id":11,"type":"discord","durationMs":118} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:28:47.109Z info: Notification delivered successfully +{"requestId":"8eabae79","id":12,"type":"discord","durationMs":122} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 1) + + console.log + 2026-10-16T23:28:47.120Z info: Scheduler batch complete +{"requestId":"8eabae79","processorId":"test-uuid-0003","count":2,"durationMs":595} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.125Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.137Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Scheduler worker concurrency › concurrency=3 processes up to 3 notifications in parallel, all exactly once + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   102 | }); +  103 | + > 104 | test('concurrency=3 processes up to 3 notifications in parallel, +all exactly once', async () => { +  | ^ +  105 | await seedDueNotifications(6); +  106 | await sleep(1300); +  107 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:104:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +ts-jest[ts-compiler] (WARN) src/__tests__/blockchain-to-notification-e2e.test.ts:153:34 - +error TS2345: Argument of type '{ events: never[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: never[]; cursor: string; }' is missing the following properties from type 'GetEventsResponse': +latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +153 expect(validateRpcResponse(response).valid).toBe(true); +   ~~~~~~~~ +src/__tests__/blockchain-to-notification-e2e.test.ts:867:42 - error TS2345: +Argument of type '{ events: StellarSDK.rpc.Api.EventResponse[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: EventResponse[]; cursor: string; }' is missing the following properties from type +'GetEventsResponse': latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +867 const rpcCheck = validateRpcResponse(rpcResponse); +   ~~~~~~~~~~~ +FAIL src/services/execution-metrics.test.ts (57.16 s) + ● Console + + console.log + 2026-10-16T23:28:31.795Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.969Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.974Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:34.990Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:35.022Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:35.356Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:35.508Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:36.174Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:36.223Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:36.369Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.727Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.774Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.779Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.859Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:38.980Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.039Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.109Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.225Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.421Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.621Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.903Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:39.913Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.185Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error 3"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.190Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.427Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.564Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.431Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.571Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.651Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.685Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.835Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.983Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.058Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.250Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:43.523Z info: Notification marked for retry or failed +{"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.187Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.317Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.454Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.774Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.004Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.507Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.723Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.932Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.103Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.542Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Error 2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.547Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.725Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.732Z error: Database get error {"sql":"SELECT * FROM scheduled_notifications WHERE +id = ?","params":[6],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.740Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-me +trics.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.101Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.110Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.143Z error: Database get error {"sql":"SELECT * FROM scheduled_notifications WHERE +id = ?","params":[6],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.253Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.097Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.122Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.290Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.639Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.861Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.006Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.143Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.484Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.990Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:51.111Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:51.286Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:51.851Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.141Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.393Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.542Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.498Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.634Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.743Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.751Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.780Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.840Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.912Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.015Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.023Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   199 | }); +  200 | + > 201 | it('should return retry distribution breakdown', +async () => { +  | ^ +  202 | // Create notifications with different retry counts +  203 | // 0 retries: 2 successes +  204 | for (let i = 0; i < +2; i++) { + + at src/services/execution-metrics.test.ts:201:5 + at Object. (src/services/execution-metrics.test.ts:13:9) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + + ● Execution Metrics Deduplication › should handle empty database gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 2 + +   369 | const metrics = await +repository.getExecutionMetrics(); +  370 | + > 371 | +expect(metrics.totalNotifications).toBe(0); +  | ^ +  372 | expect(metrics.successfulFirstAttempt).toBe(0); +  373 | expect(metrics.successfulAfterRetry).toBe(0); +  374 | expect(metrics.permanentFailures).toBe(0); + + at Object. (src/services/execution-metrics.test.ts:371:40) + + ● Execution Metrics Deduplication › should only count COMPLETED and FAILED notifications, not PENDING + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   410 | +  411 | // Should only count the completed notification + > 412 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  413 | expect(metrics.successfulFirstAttempt).toBe(1); +  414 | }); +  415 | }); + + at Object. (src/services/execution-metrics.test.ts:412:40) + +FAIL src/api/queue-operational-metrics.test.ts (62.336 s) + ● Console + + console.log + 2026-10-16T23:28:38.681Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193316201-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.105Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.132Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.154Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193316201-1.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.435Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:40.784Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193320474-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.788Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.798Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.807Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193320474-2.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.894Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:41.990Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.586Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:42.952Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193322877-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.701Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.720Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.887Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193322877-3.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:44.938Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.019Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.329Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:45.399Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193325393-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.539Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.579Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.590Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193325393-4.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.705Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:46.878Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.105Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:47.124Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193327119-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.124Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.129Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.133Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193327119-5.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.688Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.790Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.970Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:48.974Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.018Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:49.068Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193329061-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.178Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.185Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.189Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193329061-6.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.210Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:50.563Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193330517-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.139Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.165Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.268Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193330517-7.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.371Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:52.538Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.147Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.642Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"8266193f","correlationId":"cc34c0b7-9e78-4508-a9d2-1ae63b7d292b","durationMs":23} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.777Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:53.793Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193333789-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.618Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.623Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.628Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193333789-8.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.646Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"d8ff2e5e","correlationId":"c7e9785a-fbde-4fdc-b70e-382de83909e3","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.659Z info: Handling GET /api/schedule/queue/metrics +{"requestId":"febf4c12","correlationId":"8809f095-6570-4d75-861b-bca9403f8af1","durationMs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.697Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:54.709Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193334705-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:55.339Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:55.347Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:55.351Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193334705-9.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:55.390Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:55.435Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193335431-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:56.071Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:56.087Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:56.094Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193335431-10.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:56.115Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:56.188Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:56.202Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193336199-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:57.176Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:57.243Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:57.259Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792193336199-11.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:57.862Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:58.211Z info: NotificationHealthMonitor started {"intervalMs":60000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:58.401Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:28:58.686Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +ts-jest[ts-compiler] (WARN) +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:164:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +164 { baseDelayMs: 1000, maxRetries: 3, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:203:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +203 { baseDelayMs: 1000, maxRetries: 1, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +PASS src/__tests__/database-recovery.test.ts (76.999 s) + ● Console + + console.log + 2026-10-16T23:28:59.388Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.407Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.413Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.422Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.455Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('after')","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.459Z warn: Database connection failure detected; attempting recovery +{"path":"./data/test-db-recovery.db","attempt":1,"maxReconnectAttempts":3,"delayMs":10,"error":"SQLITE_MISUSE: +Database is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.486Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.489Z warn: Database connection recovered {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.597Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:02.624Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.069Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.082Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.084Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.562Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.619Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"maxReconnectAttempts":2,"delayMs":1,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.674Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.682Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.747Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.822Z warn: Database connection failure detected; attempting recovery +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"maxReconnectAttempts":2,"delayMs":2,"error":"SQLITE_CANTOPEN: +unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.873Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:04.994Z error: Database reconnect attempt failed +{"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"error":"SQLITE_CANTOPEN: unable to open database file"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:05.025Z error: Failed to connect to database +{"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:05.092Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:07.955Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:08.076Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:08.110Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:08.885Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('dup')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:08.897Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:08.911Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.607Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.616Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.640Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.695Z error: Database run error {"sql":"INSERT INTO t (v) VALUES +('kept')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.698Z error: Transaction rolled back +{"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.718Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.814Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:13.985Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:13.991Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:14.001Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:14.009Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +ts-jest[ts-compiler] (WARN) src/services/concurrent-event-processing.test.ts:74:40 - +error TS2352: Conversion of type '{ address: string; name: string; }' to type 'ContractConfig' may +be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the +expression to 'unknown' first. + Property 'events' is missing in type '{ address: string; name: string; }' but required in type 'ContractConfig'. + +74 const contractConfig: ContractConfig = { address: CONTRACT, name: 'Concurrency' } as ContractConfig; +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/types/index.ts:18:3 + 18 events: string[]; +    ~~~~~~ + 'events' is declared here. +ts-jest[ts-compiler] (WARN) src/__tests__/stress.test.ts:631:52 - error +TS2339: Property 'totalSent' does not exist on type '{ timeoutCount: number; acceptedRequests: number; +skippedDuplicates: number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: +number; hitRatio: number; totalChecks: number; }'. + +631 console.log(` Total Sent: ${metrics.totalSent}`); +   ~~~~~~~~~ +src/__tests__/stress.test.ts:632:52 - error TS2339: Property +'duplicatesSkipped' does not exist on type '{ timeoutCount: number; acceptedRequests: number; skippedDuplicates: +number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: number; hitRatio: +number; totalChecks: number; }'. + +632 console.log(` Duplicates Skipped: ${metrics.duplicatesSkipped}`); +   ~~~~~~~~~~~~~~~~~ +FAIL src/api/execution-metrics-api.test.ts (35.173 s) + ● Console + + console.log + 2026-10-16T23:29:09.441Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.715Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.815Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.889Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:11.926Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:12.137Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:12.168Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:12.288Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:16.577Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:16.617Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:16.632Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:16.635Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.658Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.695Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.700Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.704Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.887Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:27.084Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:27.087Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:27.187Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Execution Metrics API Integration › GET /api/schedule/execution-metrics should return deduplicated metrics for +retried notifications + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › GET /api/schedule/execution-metrics should return deduplicated metrics for +retried notifications + + TypeError: Cannot read properties of undefined (reading 'close') + +   55 | // Close server +  56 | await new +Promise<void>((resolve, reject) => { + > 57 | server.close((err) => { +  | ^ +  58 | if (err) reject(err); +  59 | else resolve(); +  60 | }); + + at src/api/execution-metrics-api.test.ts:57:14 + at Object. (src/api/execution-metrics-api.test.ts:56:11) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + Server is not running. + + ● Execution Metrics API Integration › should return 503 when scheduler is not enabled + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › should return 503 when scheduler is not enabled + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › should return 503 when scheduler is not enabled + + Server is not running. + + ● Execution Metrics API Integration › should handle CORS preflight requests + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › should handle CORS preflight requests + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › should handle CORS preflight requests + + Server is not running. + + ● Execution Metrics API Integration › GET /api/schedule/stats should still work for notification-level statistics + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › GET /api/schedule/stats should still work for notification-level statistics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › GET /api/schedule/stats should still work for notification-level statistics + + Server is not running. + +FAIL src/database/query-performance.test.ts (42.008 s) + ● Console + + console.log + 2026-10-16T23:29:31.756Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792193371728.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.568Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.602Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.645Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792193371728.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.235Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.268Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status, priority)\n VALUES (?, 'discord', ?, +datetime('now', ?), 'PENDING', ?)","params":["{\"i\":73}","user-73","-73 +seconds",4],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.349Z warn: Database connection failure detected; attempting recovery {"path":"C:\\U +sers\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792193371728.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100, +"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   44 | }); +  45 | + > 46 | it('uses an indexed plan for pending claim queries', +async () => { +  | ^ +  47 | // Seed enough rows that a full scan would be meaningful +  48 | for (let i = 0; i < +200; i++) { +  49 | await db.run( + + at src/database/query-performance.test.ts:46:3 + at Object. (src/database/query-performance.test.ts:20:1) + +FAIL src/services/retry-deduplication.test.ts (111.599 s) + ● Console + + console.log + 2026-10-16T23:29:05.751Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:09.853Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:09.911Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:10.148Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:10.537Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:14.974Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.186Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.293Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.532Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.536Z error: Database run error {"sql":"\n UPDATE scheduled_notifications\n + SET \n status = ?,\n retry_count = ?,\n last_error = ?,\n error_details = ?,\n +next_retry_at = ?,\n processing_completed_at = ?,\n updated_at = ?,\n processor_id = NULL,\n + lock_expires_at = NULL\n WHERE id = ?\n ","params":["DEAD_LETTERED",2,"Final failure","{\"message\":\"Final +failure\",\"stack\":\"Error: Final failure\\n at Object. (C:\\\\Users\\\\NEW +OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.t +est.ts:81:11)\",\"timestamp\":\"2026-10-16T23:29:15.530Z\"}",null,"2026-10-16T23:29:15.530Z","2026-10-16T23:29:15.530Z" +,1],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.543Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.735Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.742Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.753Z error: Database run error {"sql":"\n UPDATE scheduled_notifications\n + SET \n status = ?,\n retry_count = ?,\n last_error = ?,\n error_details = ?,\n +next_retry_at = ?,\n processing_completed_at = ?,\n updated_at = ?,\n processor_id = NULL,\n + lock_expires_at = NULL\n WHERE id = ?\n ","params":["DEAD_LETTERED",2,"Final failure","{\"message\":\"Final +failure\",\"stack\":\"Error: Final failure\\n at Object. (C:\\\\Users\\\\NEW +OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.t +est.ts:81:11)\",\"timestamp\":\"2026-10-16T23:29:15.530Z\"}",null,"2026-10-16T23:29:15.530Z","2026-10-16T23:29:15.530Z" +,1],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:15.860Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:17.729Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:17.809Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:17.820Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.115Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.331Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.515Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.649Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.715Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.720Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:18.758Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:19.300Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:19.610Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:20.188Z warn: Notification moved to dead letter queue +{"id":2,"notificationType":"discord","failureReason":"Second attempt failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:20.198Z info: Notification marked for retry or failed +{"id":2,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:20.522Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:20.697Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:20.877Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.002Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.016Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.020Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.105Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.263Z info: Notification marked as completed {"id":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.356Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.666Z info: Notification marked for retry or failed +{"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.759Z info: Notification marked as completed {"id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:21.908Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:22.227Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:22.293Z info: Notification marked for retry or failed +{"id":5,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:22.535Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:22.573Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:22.656Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:22.817Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.014Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.018Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.066Z warn: Notification moved to dead letter queue +{"id":6,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.073Z info: Notification marked for retry or failed +{"id":6,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.116Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.207Z info: Notification marked as completed {"id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.270Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.410Z info: Notification marked for retry or failed +{"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.542Z info: Notification marked as completed {"id":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.641Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.717Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.836Z info: Notification marked for retry or failed +{"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.885Z info: Notification marked as completed {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.904Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.940Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:23.977Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.138Z warn: Notification moved to dead letter queue +{"id":10,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.143Z info: Notification marked for retry or failed +{"id":10,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.343Z warn: Notification moved to dead letter queue +{"id":10,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.418Z info: Notification marked for retry or failed +{"id":10,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.590Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.811Z info: Notification marked as completed {"id":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:24.984Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.192Z info: Notification marked for retry or failed +{"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.530Z info: Notification marked as completed {"id":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.552Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.580Z info: Notification marked for retry or failed +{"id":13,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.908Z info: Notification marked for retry or failed +{"id":13,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.948Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:25.968Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.187Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.199Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.202Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[14,2,"RETRY","Attempt 2 +failed",null,1200],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.208Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.365Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.369Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.524Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.588Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.800Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.805Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.881Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:26.926Z warn: Notification moved to dead letter queue +{"id":14,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:27.067Z info: Notification marked for retry or failed +{"id":14,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:27.634Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:28.099Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:28.591Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:28.655Z warn: Notification moved to dead letter queue +{"id":14,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:28.689Z info: Notification marked for retry or failed +{"id":14,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.073Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.081Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.738Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.769Z info: Notification marked as completed {"id":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.828Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.842Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.861Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[19,1,"RETRY","Temporary +error",null,1000],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:29.883Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.307Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.341Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.346Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.520Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.523Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.554Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.782Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.787Z info: Notification marked for retry or failed +{"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.009Z info: Notification marked as completed {"id":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.066Z info: Notification marked as completed {"id":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.178Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.200Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.281Z info: Notification cancelled {"id":21} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.389Z info: Notification marked for retry or failed +{"id":22,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.409Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.426Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[22,2,"RETRY","Attempt 2 +failed",null,700],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.443Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.669Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.678Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.718Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.871Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.891Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.939Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.993Z info: Notification marked for retry or failed +{"id":22,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.055Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.107Z info: Notification marked as completed {"id":23} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.191Z info: Notification marked as completed {"id":22} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.669Z info: Scheduled notification created {"id":24,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.851Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.867Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[24,1,"RETRY","Attempt 1 +failed",null,1100],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:32.877Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.078Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.094Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.100Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.196Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.233Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.292Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.317Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.605Z info: Scheduled notification created {"id":25,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.633Z info: Notification marked for retry or failed +{"id":24,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.857Z info: Scheduled notification created {"id":26,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.715Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.723Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.777Z info: Notification marked as completed {"id":25} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.937Z info: Notification marked as completed {"id":26} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.957Z warn: Notification moved to dead letter queue +{"id":24,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:34.973Z info: Notification marked for retry or failed +{"id":24,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.006Z info: Scheduled notification created {"id":27,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.011Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.051Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n +","params":[27,1,"SUCCESS",null,null,500],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.058Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.260Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.364Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.441Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.679Z info: Notification marked as completed {"id":27} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.731Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.772Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.789Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.879Z info: Scheduled notification created {"id":28,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:35.902Z info: Scheduled notification created {"id":29,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.043Z info: Notification marked for retry or failed +{"id":28,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.060Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":1,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.151Z info: Notification marked as completed {"id":28} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.171Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":2,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.253Z info: Scheduled notification created {"id":30,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.671Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":3,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:36.776Z info: Notification marked for retry or failed +{"id":30,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.603Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":4,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.653Z info: Notification marked for retry or failed +{"id":30,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.691Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":5,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.734Z info: Notification marked as completed {"id":30} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.784Z info: Scheduled notification created {"id":31,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.788Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":6,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.849Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.853Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":7,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.933Z info: Notification marked for retry or failed +{"id":31,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:38.939Z info: Notification marked for retry or failed +{"id":29,"newStatus":"PENDING","retryCount":8,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.037Z warn: Notification moved to dead letter queue +{"id":31,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.051Z info: Notification marked for retry or failed +{"id":31,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.055Z warn: Notification moved to dead letter queue +{"id":29,"notificationType":"discord","failureReason":"Attempt 9 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.058Z info: Notification marked for retry or failed +{"id":29,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.117Z info: Notification marked as completed {"id":29} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.139Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.151Z error: Database run error {"sql":"\n INSERT INTO dead_letter_queue (\n + scheduled_notification_id, notification_type, target_recipient, payload, failure_reason, error_details, +retry_count\n ) VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(scheduled_notification_id) DO UPDATE SET\n +failure_reason = excluded.failure_reason,\n error_details = excluded.error_details,\n retry_count = +excluded.retry_count,\n last_retried_at = NULL\n +","params":[31,"discord","webhook-url","{\"test\":\"batch-19\"}","Attempt 4 failed","{\"message\":\"Attempt 4 +failed\",\"stack\":\"Error: Attempt 4 failed\\n at Object. (C:\\\\Users\\\\NEW +OWNER\\\\Documents\\\\drips\\\\core foundry 941\\\\Notify-Chain\\\\listener\\\\src\\\\services\\\\retry-deduplication.t +est.ts:277:13)\",\"timestamp\":\"2026-10-16T23:29:39.083Z\"}",4],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.158Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.286Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.370Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.384Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.594Z warn: Notification moved to dead letter queue +{"id":31,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.619Z info: Notification marked for retry or failed +{"id":31,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.634Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.639Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.653Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.661Z info: Scheduled notification created {"id":32,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.708Z info: Scheduled notification created {"id":33,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.788Z info: Notification marked as completed {"id":32} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.792Z info: Notification marked as completed {"id":33} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.840Z info: Scheduled notification created {"id":34,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.849Z info: Scheduled notification created {"id":35,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.018Z info: Notification marked for retry or failed +{"id":34,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.120Z info: Notification marked as completed {"id":35} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.169Z info: Scheduled notification created {"id":36,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.233Z info: Notification marked as completed {"id":34} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.289Z info: Scheduled notification created {"id":37,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.299Z info: Notification marked as completed {"id":36} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.427Z info: Scheduled notification created {"id":38,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.576Z info: Notification marked for retry or failed +{"id":37,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.697Z info: Notification marked as completed {"id":38} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.734Z info: Notification marked for retry or failed +{"id":37,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.741Z info: Scheduled notification created {"id":39,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.995Z info: Notification marked as completed {"id":37} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.004Z info: Notification marked as completed {"id":39} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.021Z info: Scheduled notification created {"id":40,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.038Z info: Scheduled notification created {"id":41,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.136Z info: Notification marked for retry or failed +{"id":40,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.140Z info: Notification marked for retry or failed +{"id":41,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.225Z info: Notification marked as completed {"id":40} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.235Z info: Notification marked for retry or failed +{"id":41,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.344Z info: Scheduled notification created {"id":42,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.811Z info: Notification marked for retry or failed +{"id":42,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.874Z warn: Notification moved to dead letter queue +{"id":41,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.879Z info: Notification marked for retry or failed +{"id":41,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.894Z info: Notification marked as completed {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.110Z info: Scheduled notification created {"id":43,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.621Z info: Notification marked for retry or failed +{"id":43,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.626Z warn: Notification moved to dead letter queue +{"id":41,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.633Z info: Notification marked for retry or failed +{"id":41,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.686Z info: Scheduled notification created {"id":44,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.754Z info: Notification marked as completed {"id":43} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.833Z info: Scheduled notification created {"id":45,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.849Z info: Notification marked as completed {"id":44} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.886Z info: Scheduled notification created {"id":46,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.915Z info: Notification marked for retry or failed +{"id":45,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.943Z info: Notification marked for retry or failed +{"id":46,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.974Z info: Notification marked for retry or failed +{"id":45,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.002Z info: Notification marked as completed {"id":46} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.051Z info: Notification marked as completed {"id":45} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.058Z info: Scheduled notification created {"id":47,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.092Z info: Scheduled notification created {"id":48,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.132Z info: Notification marked for retry or failed +{"id":47,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.179Z info: Notification marked for retry or failed +{"id":48,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.335Z info: Notification marked for retry or failed +{"id":47,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.520Z info: Notification marked for retry or failed +{"id":48,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.568Z info: Notification marked as completed {"id":47} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.606Z info: Notification marked as completed {"id":48} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.616Z info: Scheduled notification created {"id":49,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.644Z info: Scheduled notification created {"id":50,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.773Z info: Notification marked for retry or failed +{"id":49,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.822Z info: Notification marked for retry or failed +{"id":50,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.858Z info: Notification marked for retry or failed +{"id":49,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.891Z info: Notification marked for retry or failed +{"id":50,"newStatus":"PENDING","retryCount":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.970Z warn: Notification moved to dead letter queue +{"id":49,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.974Z info: Notification marked for retry or failed +{"id":49,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.125Z warn: Notification moved to dead letter queue +{"id":50,"notificationType":"discord","failureReason":"Attempt 3 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.149Z info: Notification marked for retry or failed +{"id":50,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.252Z warn: Notification moved to dead letter queue +{"id":50,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.255Z info: Notification marked for retry or failed +{"id":50,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.326Z warn: Notification moved to dead letter queue +{"id":49,"notificationType":"discord","failureReason":"Attempt 4 failed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.420Z info: Notification marked for retry or failed +{"id":49,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.460Z info: Scheduled notification created {"id":51,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.469Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.483Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n +","params":[51,1,"SUCCESS",null,null,500],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.488Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup. +db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   47 |  * Ensures that a notification with max retries (3 attempts) only counts as 1 +failure +  48 |  */ + > 49 | it('should count max-retry exhausted notification as exactly 1 +failure', async () => { +  | ^ +  50 | const notificationId = await repository.create({ +  51 | payload: { test: 'max-retries' }, +  52 | notificationType: +NotificationType.DISCORD, + + at src/services/retry-deduplication.test.ts:49:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   175 | const metrics = await +repository.getExecutionMetrics(); +  176 | + > 177 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  178 | expect(metrics.successfulFirstAttempt).toBe(0); +  179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:177:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude PENDING and PROCESSING notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 13 + +   345 | +  346 | // Should only count the completed notification + > 347 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  348 | expect(metrics.successfulFirstAttempt).toBe(1); +  349 | }); +  350 | + + at Object. (src/services/retry-deduplication.test.ts:347:40) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 15 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 17 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 19 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 23 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   537 |  * Ensures getRetryDistribution() also uses deduplication +  538 |  */ + > 539 | it('should provide accurate retry distribution without +double-counting', async () => { +  | ^ +  540 | // Create notifications with specific retry patterns +  541 | const patterns = [ +  542 | { retries: 0, shouldSucceed: +true, count: 5 }, // 5 immediate successes + + at src/services/retry-deduplication.test.ts:539:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (120.242 s) + ● Console + + console.log + 2026-10-16T23:29:29.566Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.387Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.840Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:30.898Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.916Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.918Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.921Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:31.925Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.102Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.105Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.107Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:33.110Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.189Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.298Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.432Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.437Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.189Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.199Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.202Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.204Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.112Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.156Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.168Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.170Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.915Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.945Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.959Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.963Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.523Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.525Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.527Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.529Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/services/concurrent-event-processing.test.ts (136.38 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 9) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   311 | } +  312 | + > 313 | it('two listener instances polling the same events notify each +event exactly once', async () => { +  | ^ +  314 | const instanceA = new +EventSubscriber(makeConfig(), new EventDeduplicationService(dbA)); +  315 | const instanceB = new +EventSubscriber(makeConfig(), new EventDeduplicationService(dbB)); +  316 | + + at src/services/concurrent-event-processing.test.ts:313:5 + at src/services/concurrent-event-processing.test.ts:286:3 + at Object. (src/services/concurrent-event-processing.test.ts:125:1) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/scheduled-notification-queue.test.ts (78.816 s) + ● Console + + console.log + 2026-10-16T23:29:37.177Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.060Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.072Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.095Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.127Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.136Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.142Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.199Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.201Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.203Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.225Z info: Fetched and locked pending notifications +{"count":1,"processorId":"restarted-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.238Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:39.259Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.417Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.427Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.464Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.580Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:40.900Z info: Fetched and locked pending notifications +{"count":1,"processorId":"doomed-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.121Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.140Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.308Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.325Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.342Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.724Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:41.869Z info: Fetched and locked pending notifications +{"count":1,"processorId":"next-worker"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.008Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:42.259Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.906Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.910Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:43.919Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.088Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.156Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.191Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.202Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.417Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.456Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.464Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.469Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.781Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:44.926Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:45.777Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:46.216Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:46.352Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:46.691Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:47.164Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:47.356Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:47.797Z info: Fetched and locked pending notifications +{"count":3,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:48.001Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:49.312Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:51.584Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:51.681Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:51.769Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.108Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.834Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:52.868Z info: Fetched and locked pending notifications +{"count":2,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.220Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.655Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:53.932Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:59.300Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:59.415Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:59.483Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:59.688Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:59.866Z error: Database run error {"sql":"DELETE FROM +notification_execution_log","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:29:59.951Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-no +tification-queue.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is +closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:03.545Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:03.711Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:04.166Z error: Database run error {"sql":"DELETE FROM +notification_execution_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:05.079Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:08.808Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:09.351Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:09.793Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-no +tification-queue.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is +closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:10.405Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:10.517Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:10.585Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:10.941Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:10.954Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:10.968Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:13.084Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:13.090Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:13.097Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:13.644Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:13.843Z info: Fetched and locked pending notifications +{"count":1,"processorId":"worker-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:14.265Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent outage"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:14.285Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:14.381Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Durable notification queue › refuses to renew a lease owned by another worker or already finished + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   36 | let repository: ScheduledNotificationRepository; +  37 | + > 38 | beforeEach(async () => { +  | ^ +  39 | await removeTestDatabase(); +  40 | +  41 | db = new Database(testDbPath); + + at src/services/scheduled-notification-queue.test.ts:38:3 + at Object. (src/services/scheduled-notification-queue.test.ts:34:1) + + ● Durable notification queue › does not dequeue a retry that is still inside its backoff window + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   36 | let repository: ScheduledNotificationRepository; +  37 | + > 38 | beforeEach(async () => { +  | ^ +  39 | await removeTestDatabase(); +  40 | +  41 | db = new Database(testDbPath); + + at src/services/scheduled-notification-queue.test.ts:38:3 + at Object. (src/services/scheduled-notification-queue.test.ts:34:1) + + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (139.223 s) + ● Notification delivery lifecycle (e2e) › happy path delivery › delivers a contract event to Discord and marks +fingerprint as sent + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   93 | // ========================================================================= +  94 | describe('happy path delivery', () => { + > 95 | it('delivers a contract event to Discord and marks fingerprint as +sent', async () => { +  | ^ +  96 | const service = new +DiscordNotificationService(discordConfig); +  97 | const result = await +service.sendEventNotification(makeEvent(), contractCfg, 'req-happy'); +  98 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:95:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › includes ledger number in the embed + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   114 | }); +  115 | + > 116 | it('includes ledger number in the embed', +async () => { +  | ^ +  117 | const service = new +DiscordNotificationService(discordConfig); +  118 | await service.sendEventNotification( +  119 | makeEvent({ txHash: 'tx-abc123', ledger: +42000 }), + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:116:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › returns true and does not call fetch for duplicate +events (deduplication) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   131 | }); +  132 | + > 133 | it('returns true and does not call fetch for duplicate events +(deduplication)', async () => { +  | ^ +  134 | const service = new +DiscordNotificationService(discordConfig); +  135 | const event = makeEvent({ id: 'evt-dedup-1' +}); +  136 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:133:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders contract events with color 0xff00 + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders system events with color 0x99ff + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles multi-symbol topic arrays + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   253 | // ========================================================================= +  254 | describe('event topic rendering', () => { + > 255 | it('handles multi-symbol topic arrays', +async () => { +  | ^ +  256 | const service = new +DiscordNotificationService(discordConfig); +  257 | const event = makeEvent({ +  258 | topic: +[xdr.ScVal.scvSymbol('task'), +xdr.ScVal.scvSymbol('completed')], + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:255:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:254:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   284 | // ========================================================================= +  285 | describe('network errors', () => { + > 286 | it('returns false and logs error when fetch +throws', async () => { +  | ^ +  287 | fetchMock.mockRejectedValueOnce(new +Error('ECONNRESET')); +  288 | +  289 | const service = new +DiscordNotificationService(discordConfig); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:286:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   304 | }); +  305 | + > 306 | it('returns false on timeout (abort)', +async () => { +  | ^ +  307 | // Simulate a slow response that gets aborted +  308 | fetchMock.mockImplementationOnce( +  309 | () => + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:306:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › allows same event after dedup window expires + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   330 | // ========================================================================= +  331 | describe('deduplication', () => { + > 332 | it('allows same event after dedup window +expires', async () => { +  | ^ +  333 | const dedup = new NotificationDeduplicator({ +  334 | windowMs: 100, +  335 | maxSize: 100, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:332:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › evicts oldest entry when cache is at capacity + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   355 | }); +  356 | + > 357 | it('evicts oldest entry when cache is at +capacity', async () => { +  | ^ +  358 | const dedup = new NotificationDeduplicator({ +maxSize: 2, windowMs: 60000 }); +  359 | const service = new +DiscordNotificationService(discordConfig, dedup); +  360 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:357:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › delivery report › DiscordNotificationService tracks metrics via +deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   409 | // ========================================================================= +  410 | describe('delivery report', () => { + > 411 | it('DiscordNotificationService tracks metrics via +deduplicator', async () => { +  | ^ +  412 | const service = new +DiscordNotificationService(discordConfig); +  413 | +  414 | await service.sendEventNotification(makeEvent({ id: +'evt-metrics-1' }), contractCfg); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:411:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:410:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/services/discord-notification.test.ts (64.629 s) + ● DiscordNotificationService › sendEventNotification › should send event notification successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   58 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  59 | + > 60 | expect(result).toBe(true); +  | ^ +  61 | expect(mockFetch).toHaveBeenCalledTimes(1); +  62 | const [url, options] = +mockFetch.mock.calls[0]; +  63 | expect(url).toBe(mockConfig.webhookUrl); + + at Object. (src/services/discord-notification.test.ts:60:22) + + ● DiscordNotificationService › sendEventNotification › should handle request timeout + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Discord webhook request timed out", ObjectContaining {"timeoutMs": 100, "webhookId": "123"} + Received + 1: "Discord webhook request error", {"attempt": 0, "contractAddress": "CA123", "correlationId": +"baa1b6e4-2376-437b-b785-6fb071a66855", "durationMs": 311, "error": [DnsRebindingError: DNS resolution returned no +addresses for hostname: discord.com], "eventId": "event-123", "requestId": "baa1b6e4-2376-437b-b785-6fb071a66855", +"webhookId": "123"} + 2: "Exceeded max Discord retry attempts", {"contractAddress": "CA123", "correlationId": +"baa1b6e4-2376-437b-b785-6fb071a66855", "eventId": "event-123", "maxRetries": 0, "requestId": +"baa1b6e4-2376-437b-b785-6fb071a66855", "webhookId": "123"} + + Number of calls: 2 + +   111 | +  112 | expect(result).toBe(false); + > 113 | expect(mockLoggerModule.error).toHaveBeenCalledWith( +  | ^ +  114 | 'Discord webhook request timed out', +  115 | expect.objectContaining({ +  116 | webhookId: mockConfig.webhookId, + + at Object. (src/services/discord-notification.test.ts:113:38) + + ● DiscordNotificationService › duplicate detection › skips the webhook call for a duplicate event + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   133 | const secondResult = await +service.sendEventNotification(mockEvent, mockContractConfig); +  134 | + > 135 | +expect(mockFetch).toHaveBeenCalledTimes(1); +  | ^ +  136 | expect(secondResult).toBe(true); +  137 | expect(service.getDeduplicationMetrics()).toEqual( +  138 | expect.objectContaining({ skippedDuplicates: 1, +cacheSize: 1 }), + + at Object. (src/services/discord-notification.test.ts:135:25) + + ● DiscordNotificationService › duplicate detection › logs a duplicate detection event + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping duplicate notification", ObjectContaining {"contractAddress": "CA123", "eventId": +"event-dup-log"} + Received + 1: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"fd3ec92b-260f-4ba8-a4c8-e04a3ea73e7d", "eventId": "event-dup-log", "requestId": +"fd3ec92b-260f-4ba8-a4c8-e04a3ea73e7d", "webhookId": "123"} + 2: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"4e30c2b1-4ef9-4f84-b4b2-4c6515b27851", "eventId": "event-dup-log", "requestId": +"4e30c2b1-4ef9-4f84-b4b2-4c6515b27851", "webhookId": "123"} + + Number of calls: 2 + +   151 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  152 | + > 153 | expect(mockLoggerModule.info).toHaveBeenCalledWith( +  | ^ +  154 | 'Skipping duplicate notification', +  155 | expect.objectContaining({ +  156 | eventId: 'event-dup-log', + + at Object. (src/services/discord-notification.test.ts:153:37) + + ● DiscordNotificationService › duplicate detection › allows the same notification request after the configured +window expires + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   173 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  174 | + > 175 | expect(result).toBe(true); +  | ^ +  176 | expect(mockFetch).toHaveBeenCalledTimes(2); +  177 | expect(service.getDeduplicationMetrics()).toEqual( +  178 | expect.objectContaining({ acceptedRequests: 2, +skippedDuplicates: 1 }), + + at Object. (src/services/discord-notification.test.ts:175:22) + + ● DiscordNotificationService › duplicate detection › allows the same event id on a different contract through + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 0 + +   191 | const result = await +service.sendEventNotification(mockEvent, contractB); +  192 | + > 193 | +expect(mockFetch).toHaveBeenCalledTimes(2); +  | ^ +  194 | expect(result).toBe(true); +  195 | }); +  196 | + + at Object. (src/services/discord-notification.test.ts:193:25) + + ● DiscordNotificationService › duplicate detection › does not mark an event as sent when the webhook call fails + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   213 | +  214 | expect(firstResult).toBe(false); + > 215 | expect(secondResult).toBe(true); +  | ^ +  216 | expect(mockFetch).toHaveBeenCalledTimes(2); +  217 | }); +  218 | + + at Object. (src/services/discord-notification.test.ts:215:28) + + ● DiscordNotificationService › duplicate detection › accepts an injected deduplicator + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   227 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  228 | + > 229 | +expect(deduplicator.size()).toBe(1); +  | ^ +  230 | +expect(deduplicator.isDuplicate(generateFingerprint('event-injected', +'CA123'))).toBe(true); +  231 | }); +  232 | }); + + at Object. (src/services/discord-notification.test.ts:229:35) + + ● DiscordNotificationService › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   239 | const result = await +service.sendTestMessage(); +  240 | + > 241 | expect(result).toBe(true); +  | ^ +  242 | }); +  243 | +  244 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification.test.ts:241:22) + + ● DiscordNotificationService › formatEventMessage › should format event with string value correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   264 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  265 | + > 266 | expect(result).toBe(true); +  | ^ +  267 | const [, options] = +mockFetch.mock.calls[0]; +  268 | const body = +JSON.parse(options.body); +  269 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:266:22) + + ● DiscordNotificationService › formatEventMessage › should truncate long string values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   283 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  284 | + > 285 | expect(result).toBe(true); +  | ^ +  286 | const [, options] = +mockFetch.mock.calls[0]; +  287 | const body = +JSON.parse(options.body); +  288 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:285:22) + + ● DiscordNotificationService › formatEventMessage › should handle symbol type values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   302 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  303 | + > 304 | expect(result).toBe(true); +  | ^ +  305 | const [, options] = +mockFetch.mock.calls[0]; +  306 | const body = +JSON.parse(options.body); +  307 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:304:22) + + ● DiscordNotificationService › formatEventMessage › should preserve short messages without truncation + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   320 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  321 | + > 322 | expect(result).toBe(true); +  | ^ +  323 | const [, options] = +mockFetch.mock.calls[0]; +  324 | const body = +JSON.parse(options.body); +  325 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:322:22) + + ● DiscordNotificationService › formatEventMessage › should truncate oversized embed titles + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   340 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  341 | + > 342 | expect(result).toBe(true); +  | ^ +  343 | const [, options] = +mockFetch.mock.calls[0]; +  344 | const body = +JSON.parse(options.body); +  345 | expect(body.embeds[0].title.length).toBeL +essThanOrEqual(256); + + at Object. (src/services/discord-notification.test.ts:342:22) + + ● DiscordNotificationService › formatEventMessage › should enforce total embed size limit + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   361 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  362 | + > 363 | expect(result).toBe(true); +  | ^ +  364 | const [, options] = +mockFetch.mock.calls[0]; +  365 | const body = +JSON.parse(options.body); +  366 | const embed = body.embeds[0]; + + at Object. (src/services/discord-notification.test.ts:363:22) + + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   380 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  381 | + > 382 | expect(result).toBe(true); +  | ^ +  383 | const [, options] = +mockFetch.mock.calls[0]; +  384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:382:22) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone +embedded in an scvString payload + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   459 | +  460 | describe('integration: mention in event string value reaches embed +sanitized', () => { + > 461 | it('strips @everyone embedded in an scvString +payload', async () => { +  | ^ +  462 | mockFetch.mockResolvedValueOnce({ ok: true }); +  463 | +  464 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:461:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone in +event topic symbol used as embed title + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   487 | }); +  488 | + > 489 | it('strips @everyone in event topic symbol used as embed +title', async () => { +  | ^ +  490 | mockFetch.mockResolvedValueOnce({ ok: true }); +  491 | +  492 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:489:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + +FAIL src/services/notification-failure-scenarios.test.ts (37.002 s) + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › tracks timeoutCount in DiscordNotificationService on +AbortError + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 1 + Received: 0 + +   71 | const success = await +service.sendTestMessage(); +  72 | expect(success).toBe(false); + > 73 | +expect(service.getMetrics().timeoutCount).toBeGreaterThanOrEqual(1); +  | ^ +  74 | }); +  75 | }); +  76 | + + at Object. (src/services/notification-failure-scenarios.test.ts:73:49) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/database/event-notification-indexes.test.ts (5.628 s) + ● Console + + console.log + 2026-10-16T23:30:21.580Z info: Connected to SQLite database +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-27936-1792193421525.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.730Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.748Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.772Z info: Database initialized successfully +{"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-27936-1792193421525.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.799Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, +created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILE +D","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.913Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/tests/template-system.test.ts (8.027 s) + ● Console + + console.log + 2026-10-16T23:30:21.406Z info: Connected to SQLite database {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.916Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:23.002Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:23.028Z info: Database initialized successfully {"path":"./data/test-templates.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:23.181Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX +IF NOT EXISTS idx_templates_unique_key \r\n ON +notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:23.239Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● Console + + console.log + 2026-10-16T23:30:21.453Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:21.671Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.780Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:22.992Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:23.403Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/database/database-transaction.test.ts (5.894 s) + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4456.171887346246, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:30:29.008Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/tests/template-api-integration.test.ts (25.466 s) + ● Console + + console.log + 2026-10-16T23:30:24.084Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:25.670Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:25.734Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.110Z info: Database initialized successfully {"path":"./data/test-template-api.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.121Z error: Database run error {"sql":"duplicate inserts with the same key are +silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.272Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue +(\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n +notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n +target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details +TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count +INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES +scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.519Z error: Database run error {"sql":"runMigrations tolerates duplicate-column +errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS +update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n +UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.577Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.655Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE +notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = +NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.710Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.763Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.892Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:26.934Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS +prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:27.019Z error: Database run error +{"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:27.581Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:27.664Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:27.771Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:27.887Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:27.975Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.030Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.082Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.104Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.148Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.332Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.338Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.341Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.345Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.352Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.359Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.407Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.421Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.426Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.456Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.526Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.541Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.567Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.593Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.628Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.661Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.730Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.772Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.775Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.780Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.784Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.788Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:29.552Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:29.593Z error: Database run error {"sql":"DELETE FROM +template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:29.744Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/tests/payload-integrity.test.ts (54.333 s) + ● Console + + console.log + 2026-10-16T23:30:26.169Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.006Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.088Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.556Z info: Database initialized successfully {"path":"./data/test-integrity.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.574Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.687Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:28.903Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:29.462Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.104Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.409Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.506Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.574Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.941Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/api/template-routes.test.ts (10.108 s) + ● Console + + console.log + 2026-10-16T23:30:30.694Z error: Failed to create template {"error":{"message":"uniqueKey: is required +and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is +required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: uniqueKey: is required and +must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required +and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:85:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.735Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:107:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.741Z error: Failed to create template {"error":{"message":"Invalid JSON +body","name":"Error","stack":"Error: Invalid JSON body\n at EventEmitter. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:42:16)\n at +EventEmitter.emit (node:events:509:28)\n at C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:26:9\n at processTicksAndRejections +(node:internal/process/task_queues:85:11)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.750Z error: Failed to create template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:141:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:137:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.756Z error: Failed to create template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:150:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:153:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.837Z error: Failed to list templates {"error":{"message":"channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one +of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.845Z info: Listed templates via API +{"requestId":"req-1","count":0,"channelType":"EMAIL","activeOnly":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.867Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:201:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.873Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:215:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.877Z error: Failed to update template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:288:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:226:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.906Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:237:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.911Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:247:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.914Z error: Failed to render template {"error":{"message":"res.setHeader is not a +function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:360:7)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:261:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.936Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:30.943Z error: Failed to get template stats {"error":{"message":"res.setHeader is not +a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at +handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections +(node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/api/events-server.test.ts (28.112 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/discord-notification-refactored.test.ts (156.845 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + >[22m 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   243 | const result = await +service.sendTestMessage(); +  244 | + > 245 | expect(result).toBe(true); +  | ^ +  246 | }); +  247 | +  248 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification-refactored.test.ts:245:22) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +ts-jest[ts-compiler] (WARN) src/services/archive.test.ts:237:7 - error +TS2741: Property 'expiresAt' is missing in type '{ originalId: number; payload: string; notificationType: string; +targetRecipient: string; executeAt: string; createdAt: string; processingCompletedAt: string; status: string; +retryCount: number; lastError: null; eventId: null; contractAddress: null; metadata: null; }' but required in type '{ +originalId: number; payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: +string | null; createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +237 { +   ~ +238 originalId: 1, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +250 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +251 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +src/services/archive.test.ts:252:7 - error TS2741: Property 'expiresAt' +is missing in type '{ originalId: number; payload: string; notificationType: string; targetRecipient: string; +executeAt: string; createdAt: string; processingCompletedAt: null; status: string; retryCount: number; lastError: +string; eventId: string; contractAddress: string; metadata: null; }' but required in type '{ originalId: number; +payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: string | null; +createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +252 { +   ~ +253 originalId: 2, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +265 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +266 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +FAIL src/utils/log-redaction.test.ts (15.171 s) + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/api/auth-protected-endpoints.test.ts (15.987 s) + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/services/archive.test.ts (19.752 s) + ● Console + + console.log + 2026-10-16T23:30:40.551Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T2 +3:30:40.549Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:41.863Z info: ArchiveService: cycle complete +{"archived":2,"purged":0,"durationMs":1315,"jobId":"archive-cycle-1792193440540"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:41.878Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.005Z info: ArchiveService: archived processed notification immediately {"id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.011Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792193442010"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.053Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.392Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792193442390"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.398Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.419Z info: ArchiveService: purged expired archive records +{"count":2,"cutoff":"2026-07-18T23:30:42.418Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.433Z info: ArchiveService: cycle complete +{"archived":0,"purged":2,"durationMs":19,"jobId":"archive-cycle-1792193442413"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.436Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.440Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792193442439"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.442Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.468Z info: ArchiveService started +{"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.468Z info: Waiting for active archival jobs to complete {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.468Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.596Z info: ArchiveService: cycle complete +{"archived":0,"purged":0,"durationMs":128,"jobId":"archive-cycle-1792193442468"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.644Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:42.753Z info: Handling GET /api/archive {"requestId":"req-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:43.453Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:43.482Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:30:43.814Z info: Handling POST /api/archive/run {"requestId":"req-6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +ts-jest[ts-compiler] (WARN) src/services/webhook-verifier.test.ts:601:20 - +error TS2339: Property 'authenticated' does not exist on type 'Promise'. + +601 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:601:20 + 601 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:602:20 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +602 expect(outcome.statusCode).toBe(401); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:602:20 + 602 expect(outcome.statusCode).toBe(401); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:603:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +603 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:603:20 + 603 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:613:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +613 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:613:20 + 613 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:628:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +628 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:628:20 + 628 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:639:21 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +639 expect(replay.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:639:21 + 639 expect(replay.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:640:21 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +640 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:640:21 + 640 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:657:18 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +657 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:657:18 + 657 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:668:19 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +668 expect(second.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:668:19 + 668 expect(second.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:669:19 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +669 expect(second.statusCode).toBe(409); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:669:19 + 669 expect(second.statusCode).toBe(409); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:670:19 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +670 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:670:19 + 670 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:695:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +695 expect(a.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:695:14 + 695 expect(a.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:696:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +696 expect(b.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:696:14 + 696 expect(b.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:709:16 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +709 expect(bad.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:709:16 + 709 expect(bad.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:718:17 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +718 expect(good.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:718:17 + 718 expect(good.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:729:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +729 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:729:20 + 729 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:730:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +730 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:730:20 + 730 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:740:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +740 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:740:20 + 740 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:741:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +741 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:741:20 + 741 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:756:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +756 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:756:20 + 756 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:770:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +770 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:770:20 + 770 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (168.472 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   169 | +  170 | describe('fan-out delivery', () => { + > 171 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  172 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  173 | +  174 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:171:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   178 | }); +  179 | + > 180 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  181 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  182 | +  183 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:180:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   194 | +  195 | describe('channel-specific payloads', () => { + > 196 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  197 | await deliverToAllChannels( +  198 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  199 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:196:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   210 | }); +  211 | + > 212 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  213 | await deliverToAllChannels( +  214 | createMockEvent({ id: 'event-system', type: +'system' }) +  215 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:212:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +FAIL src/services/notification-retry-queue-refactored.test.ts (12.807 s) + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4134.731315203808, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:30:53.688Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/services/webhook-verifier.test.ts (20.092 s) + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +ts-jest[ts-compiler] (WARN) src/services/notification-expiration.test.ts:40:9 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +40 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:45:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +45 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:46:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +46 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:63:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +63 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:68:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +68 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:69:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +69 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:86:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +86 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:91:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +91 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:92:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +92 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:116:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +116 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:121:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +121 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:122:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +122 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:140:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +140 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:145:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +145 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:146:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +146 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:164:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +164 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:169:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +169 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:170:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +170 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:199:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +199 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:204:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +204 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:205:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +205 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:222:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +222 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:227:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +227 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:228:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +228 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:340:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +340 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:345:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +345 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:346:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +346 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:379:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +379 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:384:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +384 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:385:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +385 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:407:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +407 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:412:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +412 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:413:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +413 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:435:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +435 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:440:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +440 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:441:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +441 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:467:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +467 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:472:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +472 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:473:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +473 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:503:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +503 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:508:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +508 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:509:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +509 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:521:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +521 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:526:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +526 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:527:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +527 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:552:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +552 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:557:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +557 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:558:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +558 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +FAIL src/services/notification-expiration.test.ts (14.937 s) + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/config.test.ts (29.731 s) + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/notification-deduplicator.test.ts (19.785 s) + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/cleanup-service.test.ts (12.743 s) + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/__tests__/config-startup-validation.test.ts (11.796 s) + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/services/webhook-retry-helper.test.ts (35.703 s) + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +ts-jest[ts-compiler] (WARN) src/services/retry-policy-integration.test.ts:631:7 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +631 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:658:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +658 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:680:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +680 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:702:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +702 baseDelayMs: 10, +   ~~~~~~~~~~~ +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-reorg.test.ts:62:3 - +error TS2353: Object literal may only specify known properties, and 'maxPayloadSizeBytes' does not +exist in type 'Config'. + +62 maxPayloadSizeBytes: 64 * 1024, +   ~~~~~~~~~~~~~~~~~~~ +FAIL src/api/request-duration-logger.test.ts (26.362 s) + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/event-subscriber-reorg.test.ts (21.827 s) + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "e11afc94"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "eac40715-9d47-4dc0-a6dd-783e2d43ac8f", +"eventId": "event-1", "requestId": "eac40715-9d47-4dc0-a6dd-783e2d43ac8f"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/services/providers/provider-capabilities.test.ts (25.996 s) + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/notification-claim-lease.test.ts (11.844 s) + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber.test.ts:739:9 - +error TS2353: Object literal may only specify known properties, and 'receivedAt' does not exist in +type 'Partial'. + +739 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:772:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +772 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:799:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +799 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:835:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +835 receivedAt: veryOldTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:861:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +861 receivedAt: NOW - (365 * 24 * 60 * 60 * 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:884:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +884 receivedAt: NOW - (DEFAULT_EXPIRATION_MS + 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:888:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +888 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:924:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +924 receivedAt: fastEventExpiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:928:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +928 receivedAt: NOW - 60 * 60 * 1000, // 1 hour ago (within default 24h) +   ~~~~~~~~~~ +FAIL src/services/webhook-delivery-service.test.ts (48.326 s) + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 1, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/retry-policy-integration.test.ts (45.45 s) + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/services/event-subscriber.test.ts (27.39 s) + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792107082912} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792193485593, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792107082912} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792193485593, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "ca09b146"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/tests/api-versioning.test.ts (20.304 s) + ● Console + + console.log + 2026-10-16T23:31:24.458Z info: Handling GET /api/events +{"requestId":"3b4d96d7","correlationId":"7adf9ef3-7b38-4d2c-8211-0a960f67f9a0","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:25.307Z info: GET /api/events complete +{"requestId":"3b4d96d7","correlationId":"7adf9ef3-7b38-4d2c-8211-0a960f67f9a0","returned":0,"durationMs":976} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:25.598Z info: Handling GET /api/events +{"requestId":"dcab06a7","correlationId":"dc61f076-ac82-4232-a8c4-d173efbb83ec","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:25.619Z info: GET /api/events complete +{"requestId":"dcab06a7","correlationId":"dc61f076-ac82-4232-a8c4-d173efbb83ec","returned":0,"durationMs":28} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:25.752Z info: Handling GET /api/events +{"requestId":"0c2fdbde","correlationId":"fe494eb8-170a-49d7-9cb7-3ab0b3b39a03","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:25.778Z info: GET /api/events complete +{"requestId":"0c2fdbde","correlationId":"fe494eb8-170a-49d7-9cb7-3ab0b3b39a03","returned":0,"durationMs":28} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.072Z info: Handling GET /api/events +{"requestId":"ec8bbf1a","correlationId":"352256ec-c9db-41e7-960b-91d6139cf2e2","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.081Z info: GET /api/events complete +{"requestId":"ec8bbf1a","correlationId":"352256ec-c9db-41e7-960b-91d6139cf2e2","returned":0,"durationMs":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.112Z info: Handling GET /api/events +{"requestId":"ad342623","correlationId":"b801d0d2-bad7-4868-a655-95ab54c9fdba","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.162Z info: GET /api/events complete +{"requestId":"ad342623","correlationId":"b801d0d2-bad7-4868-a655-95ab54c9fdba","returned":0,"durationMs":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.289Z info: Handling GET /api/events +{"requestId":"f4da7f2a","correlationId":"048ad4ea-f819-483d-a5f7-81045124264a","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.292Z info: GET /api/events complete +{"requestId":"f4da7f2a","correlationId":"048ad4ea-f819-483d-a5f7-81045124264a","returned":0,"durationMs":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.301Z info: Handling GET /api/events +{"requestId":"f9c603d5","correlationId":"4ebd910e-e177-42ea-8c89-4c6e3da2bce7","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.308Z info: GET /api/events complete +{"requestId":"f9c603d5","correlationId":"4ebd910e-e177-42ea-8c89-4c6e3da2bce7","returned":0,"durationMs":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.323Z info: Handling GET /api/events +{"requestId":"cea446eb","correlationId":"765c5de6-6f9c-4236-a6df-96bea2086ae8","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.327Z info: GET /api/events complete +{"requestId":"cea446eb","correlationId":"765c5de6-6f9c-4236-a6df-96bea2086ae8","returned":0,"durationMs":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.389Z info: Handling GET /api/events +{"requestId":"df65e824","correlationId":"fce7317c-bb59-4b19-8062-e89b23de4992","limit":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.394Z info: GET /api/events complete +{"requestId":"df65e824","correlationId":"fce7317c-bb59-4b19-8062-e89b23de4992","returned":0,"durationMs":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.433Z warn: Unhandled request {"requestId":"9fadde7a","correlationId":"cedcfa5c-6c9a +-44e6-9308-981e4d908f4c","method":"GET","url":"/api/v1/unknown-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.461Z info: Request completed +{"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":38,"requestId":"9fadde7a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.884Z warn: Unhandled request {"requestId":"2f43047f","correlationId":"b01be543-7e6c +-41db-a657-97f868ea1bf6","method":"GET","url":"/api/v1/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:26.892Z info: Request completed +{"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":50,"requestId":"2f43047f"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-backfill.test.ts:51:3 - +error TS1117: An object literal cannot have multiple properties with the same name. + +51 validateRpcResponse: jest.fn().mockReturnValue({ valid: true }), +   ~~~~~~~~~~~~~~~~~~~ +src/services/event-subscriber-backfill.test.ts:83:3 - error TS2741: +Property 'eventBatchSize' is missing in type '{ stellarNetwork: string; stellarNetworkPassphrase: string; +stellarRpcUrl: string; contractAddresses: ContractConfig[]; pollIntervalMs: number; maxReconnectAttempts: number; +reconnectDelayMs: number; eventsApiPort: number; eventsApiCorsOrigin: string; backfill: { ...; } | undefined; }' but +required in type 'Config'. + +83 return { +   ~~~~~~ + + src/types/index.ts:72:3 + 72 eventBatchSize: number; +    ~~~~~~~~~~~~~~ + 'eventBatchSize' is declared here. +FAIL src/services/notification-api-webhook.test.ts (13.209 s) + ● Console + + console.log + 2026-10-16T23:31:27.224Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.315Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.322Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.331Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.339Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.453Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.457Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.462Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:27.486Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +ts-jest[ts-compiler] (WARN) src/__tests__/smoke/notification-pipeline.smoke.test.ts:602:5 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +602 contractId: contractAddress, +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +FAIL src/services/event-subscriber-backfill.test.ts (28.716 s) + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts (19.326 s) + ● Console + + console.log + 2026-10-16T23:31:29.442Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:29.507Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:29.561Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:29.564Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:29.600Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":5,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/api/archive-api.test.ts (13.909 s) + ● Console + + console.log + 2026-10-16T23:31:30.087Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.578Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an +integer, received 'abc'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.624Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.627Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= +100"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.630Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.635Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= +0"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.639Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.641Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be +one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.646Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.649Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be +a valid date, received 'not-a-date'"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.653Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.781Z info: Handling GET /api/archive {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.792Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:30.831Z info: Handling POST /api/archive/run {"requestId":"req-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +PASS src/services/circuit-breaker.test.ts (8.183 s) + ● Console + + console.log + 2026-10-16T23:31:31.639Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.696Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.701Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.704Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.708Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.715Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.720Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.723Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.741Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193492740} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.745Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.749Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.753Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.757Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193492757} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.760Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.767Z warn: Circuit breaker is OPEN, blocking request +{"state":"open","nextAttemptTime":1792193492757} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.777Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.787Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.795Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193492795} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:31.805Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:33.026Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:33.034Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:33.058Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:33.061Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193494060} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:33.062Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.263Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.308Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.310Z info: Circuit breaker closed after successful recovery +{"successCount":0,"successThreshold":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.312Z info: Circuit breaker recorded success +{"state":"closed","successCount":0,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.318Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.322Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.325Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193495325} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:34.327Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.622Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt +{"previousState":"open","newState":"half_open"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.663Z info: Circuit breaker recorded success +{"state":"half_open","successCount":1,"failureCount":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.699Z error: Circuit breaker opened due to repeated failures +{"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193496698} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.701Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.763Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.767Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.769Z error: Circuit breaker opened due to repeated failures +{"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193496768} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.770Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.785Z info: Circuit breaker manually reset {"previousState":"closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.797Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":1,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.800Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":2,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.802Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":3,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.804Z warn: Circuit breaker recorded failure +{"state":"closed","failureCount":4,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.806Z error: Circuit breaker opened due to repeated failures +{"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792193496806} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.822Z warn: Circuit breaker recorded failure +{"state":"open","failureCount":5,"successCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:35.827Z info: Circuit breaker recorded success +{"state":"closed","successCount":1,"failureCount":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/config/schema-validator.test.ts (5.459 s) +PASS src/migrations/004-database-data-integrity.test.ts (10.436 s) + ● Console + + console.log + 2026-10-16T23:31:39.555Z info: Migration 004 (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.221Z info: Migration 004-repeat (database-data-integrity) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.336Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 +aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 +row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data +first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core +foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.353Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.367Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.371Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.383Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.400Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n + (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', +'2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.415Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/webhook-replay-cache.test.ts (8.678 s) +FAIL src/services/retry-scheduler-webhook.test.ts (30.429 s) + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +PASS src/services/notification-metrics-store.test.ts (8.651 s) +PASS src/utils/circuit-breaker.test.ts (9.337 s) +PASS src/api/templates-api.test.ts (23.862 s) +PASS src/services/dead-letter-queue.test.ts (11.808 s) + ● Console + + console.log + 2026-10-16T23:31:44.398Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:46.957Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:46.976Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:47.026Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:47.050Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:47.991Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"discord","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.038Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T23:31:52.053Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.077Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.112Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:51.850Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:51.860Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:51.863Z info: Database initialized successfully +{"path":"./data/test-dead-letter-queue.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:51.890Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.070Z warn: Notification moved to dead letter queue +{"id":1,"notificationType":"email","failureReason":"permanent failure"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.072Z info: Notification marked for retry or failed +{"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T23:31:56.892Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.745Z info: Dead-letter notification requeued +{"requestId":"req-2","id":1,"notificationId":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.875Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/app-version.test.ts (29.768 s) +PASS src/tests/notification-scheduler-refactored.test.ts (19.159 s) + ● Console + + console.log + 2026-10-16T23:31:41.941Z info: Connected to SQLite database +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:42.960Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:42.975Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:43.008Z info: Database initialized successfully +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:43.043Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:43.164Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:43.803Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:43.863Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:44.224Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:44.308Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:44.472Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:44.562Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:44.811Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:45.560Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:45.680Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:46.561Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:46.689Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:46.907Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:47.421Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.062Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.369Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.458Z info: Notification cancelled {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:49.101Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:49.253Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:50.254Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:50.362Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:50.556Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:50.727Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:50.873Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:51.130Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.375Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.547Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.175Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.500Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.678Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.776Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.838Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:54.089Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.180Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:56.232Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:56.500Z info: Scheduling new notification +{"type":"email","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.031Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.222Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.306Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.534Z info: Scheduling new notification +{"type":"sms","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.889Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"sms"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:58.838Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:58.997Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.576Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/json-logger-formatter.test.ts (18.477 s) +PASS src/tests/events-server.test.ts (14.302 s) + ● Console + + console.log + 2026-10-16T23:31:54.663Z info: Handling GET /api/events +{"requestId":"4cbf8964","correlationId":"6a6de04d-1b33-429d-8efb-da04f218375f","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:54.896Z info: GET /api/events complete +{"requestId":"4cbf8964","correlationId":"6a6de04d-1b33-429d-8efb-da04f218375f","returned":0,"durationMs":248} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:54.968Z info: Handling GET /api/events +{"requestId":"5ae735b3","correlationId":"my-trace-abc123","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:54.992Z info: GET /api/events complete +{"requestId":"5ae735b3","correlationId":"my-trace-abc123","returned":0,"durationMs":8} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.458Z info: Handling GET /api/events +{"requestId":"551e4d14","correlationId":"cf9ec632-64ad-4c35-b34c-f7bf70e3f383","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.593Z info: GET /api/events complete +{"requestId":"551e4d14","correlationId":"cf9ec632-64ad-4c35-b34c-f7bf70e3f383","returned":0,"durationMs":413} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.870Z warn: Unhandled request +{"requestId":"ca56e1e5","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.904Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":34,"requestId":"ca56e1e5"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.123Z warn: Unhandled request +{"requestId":"907b8264","correlationId":"ba7045f5-2bb6-474e-a801-8af8fe9b5aa6","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.130Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":11,"requestId":"907b8264"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.227Z warn: Unhandled request +{"requestId":"87ab02b6","correlationId":"e95cfa9a-95c1-445b-b9c9-897ad1be381a","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.232Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":5,"requestId":"87ab02b6"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.832Z info: Handling GET /api/events +{"requestId":"40d47112","correlationId":"f27c1e57-d979-4694-bf96-35d14a68efd4","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.840Z info: GET /api/events complete +{"requestId":"40d47112","correlationId":"f27c1e57-d979-4694-bf96-35d14a68efd4","returned":20,"durationMs":19} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.045Z info: Handling GET /api/events +{"requestId":"57e537c4","correlationId":"055b94ae-c770-421e-a0b8-656f394e87b9","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.107Z info: GET /api/events complete +{"requestId":"57e537c4","correlationId":"055b94ae-c770-421e-a0b8-656f394e87b9","returned":50,"durationMs":77} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.871Z info: Handling GET /api/events +{"requestId":"e019eca1","correlationId":"565d252b-8dea-4eb3-b35c-23d0ab649809","limit":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:00.315Z info: GET /api/events complete +{"requestId":"e019eca1","correlationId":"565d252b-8dea-4eb3-b35c-23d0ab649809","returned":100,"durationMs":470} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:03.074Z info: Handling GET /api/events +{"requestId":"cb21ae77","correlationId":"141a4ff2-329c-44cb-9e1c-a1bf3ba1fe8f","limit":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:03.808Z info: GET /api/events complete +{"requestId":"cb21ae77","correlationId":"141a4ff2-329c-44cb-9e1c-a1bf3ba1fe8f","returned":1,"durationMs":788} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:04.225Z info: Handling GET /api/events +{"requestId":"a6611d6e","correlationId":"4a1b0e56-e643-4358-9df9-0ba6551c0afe","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:04.711Z info: GET /api/events complete +{"requestId":"a6611d6e","correlationId":"4a1b0e56-e643-4358-9df9-0ba6551c0afe","returned":20,"durationMs":569} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/schema/compatibility-check.test.ts (29.1 s) +PASS src/services/worker-manager.test.ts (31.146 s) + ● Console + + console.log + 2026-10-16T23:32:11.994Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:12.751Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:12.756Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:12.799Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.196Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.202Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.205Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.209Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.221Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.224Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.234Z info: Initiating graceful shutdown {"activeJobs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:15.795Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:16.175Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:16.270Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:17.386Z info: Initiating graceful shutdown {"activeJobs":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:17.709Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:19.165Z info: All jobs completed during graceful shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:19.331Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:21.036Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:21.311Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:21.763Z warn: Graceful shutdown already initiated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:21.897Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:22.595Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:23.111Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +FAIL src/__tests__/notification-flow-e2e.test.ts (79.834 s) + ● Console + + console.log + 2026-10-16T23:31:34.295Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:36.660Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:38.416Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:39.853Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:40.572Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:42.674Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:43.136Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:44.395Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:46.478Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:48.209Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.378Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.545Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.206Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.442Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.016Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● 5. Backpressure and idempotency integration › activates backpressure when queue exceeds saturation threshold + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › calculates a positive processing delay under backpressure + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › deactivates backpressure once the queue drops below recovery +threshold + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › records a backpressure activation event and surfaces it in statistics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › computes average duration across activation/deactivation pairs + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › cleans up expired idempotency keys after 24 hours + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + + ● Test suite failed to run + + TypeError: Cannot read properties of undefined (reading 'close') + +   832 | +  833 | afterAll(async () => { + > 834 | await db.close(); +  | ^ +  835 | removeDbFile(); +  836 | }); +  837 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:834:14) + +PASS src/migrations/005-notification-expiration.test.ts (38.996 s) + ● Console + + console.log + 2026-10-16T23:32:45.447Z info: Migration 005 (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.976Z info: Migration 005-repeat (notification-expiration) applied successfully + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.063Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.270Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 +aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 +row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data +first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at +MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/rate-limiter.test.ts (52.92 s) +FAIL src/tests/notification-scheduler.test.ts (86.508 s) + ● Console + + console.log + 2026-10-16T23:31:56.100Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.510Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.724Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:59.784Z info: Database initialized successfully +{"path":"./data/test-notifications.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:02.964Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:11.069Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:11.436Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:11.799Z info: Processing scheduled notification +{"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:11.909Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":10393} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:12.657Z info: Notification marked as completed {"requestId":"test-request-id","id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:12.848Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n +","params":[1,1,"SUCCESS",null,null,655],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.039Z error: Failed to process notification {"requestId":"test-request-id","id":1,"e +rror":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}","attempt":1,"durationMs":1227} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:13.183Z error: Job monitor: job failed +{"jobId":"notification-1","jobName":"scheduled-notification","error":"SQLITE_CONSTRAINT: FOREIGN KEY constraint +failed","durationMs":1410} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:13.565Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:13.633Z info: Notification marked for retry or failed +{"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:32:18.270Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:14.183Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[1,1,"RETRY","SQLITE_CONSTRAINT: FOREIGN +KEY constraint failed",null,1227],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:14.314Z error: Error in scheduler processing loop {"requestId":"test-request-id","erro +r":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}","processorId":"receipt-success-test","durationMs":10773} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:14.738Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:14.909Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:15.361Z info: Processing scheduled notification +{"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:15.776Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":3941} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:15.952Z error: Failed to process notification +{"requestId":"test-request-id","id":2,"error":{"message":"secret +token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at +Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":581} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:16.494Z error: Job monitor: job failed +{"jobId":"notification-2","jobName":"scheduled-notification","error":"secret +token=should-not-persist","durationMs":1056} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:17.230Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:32:21.838Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:18.235Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:21.310Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":7638} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:22.937Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:27.177Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:28.061Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:28.668Z info: Expired notification skipped before delivery +{"requestId":"test-request-id","id":4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:42.831Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.147Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.242Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.252Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.261Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.267Z info: Processing scheduled notification +{"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:43.271Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1166} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:43.285Z error: Failed to process notification +{"requestId":"test-request-id","id":6,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary +outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:43.326Z error: Job monitor: job failed +{"jobId":"notification-6","jobName":"scheduled-notification","error":"temporary outage","durationMs":62} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:43.369Z info: Notification marked for retry or failed +{"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:32:43.430Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:43.503Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":549} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:44.511Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:44.527Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:44.533Z info: Processing scheduled notification +{"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.537Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":2432} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.608Z info: Notification marked as completed {"requestId":"test-request-id","id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.650Z info: Notification delivered successfully +{"requestId":"test-request-id","id":6,"type":"discord","durationMs":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.655Z info: Processing scheduled notification +{"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.659Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":2483} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.685Z info: Notification marked as completed {"requestId":"test-request-id","id":7} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.707Z info: Notification delivered successfully +{"requestId":"test-request-id","id":7,"type":"discord","durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:44.710Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":1460} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:44.722Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:44.993Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.057Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.317Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.709Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.881Z info: Fetched and locked pending notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.896Z info: Processing batch of scheduled notifications +{"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:45.910Z info: Processing scheduled notification +{"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:45.916Z warn: Missed scheduled notification detected; dispatching catch-up delivery +{"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60277} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:45.921Z error: Failed to process notification +{"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: +temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:45.926Z error: Job monitor: job failed +{"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":18} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:45.972Z info: Notification marked for retry or failed +{"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T23:32:47.947Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + at async Promise.all (index 0) + + console.log + 2026-10-16T23:32:46.108Z info: Scheduler batch complete +{"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":372} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.178Z info: Fetched due retries +{"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.211Z info: RetryScheduler processing batch +{"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.234Z info: Retrying notification +{"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.300Z info: Notification marked as completed {"requestId":"test-request-id","id":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.387Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.402Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:46.994Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.233Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.302Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.322Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.353Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.896Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.923Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.989Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.227Z info: Notification marked for retry or failed +{"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.373Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.441Z warn: Notification moved to dead letter queue +{"id":15,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.456Z info: Notification marked for retry or failed +{"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.544Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.602Z info: Notification cancelled {"id":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.714Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.772Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.945Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:48.987Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.123Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.223Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.372Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.497Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.613Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.654Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:49.960Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:50.070Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:50.218Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:50.279Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.024Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.030Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.036Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.113Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.176Z info: Notification marked as completed {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.497Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.524Z info: Notification marked for retry or failed +{"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.709Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.986Z warn: Notification moved to dead letter queue +{"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.988Z info: Notification marked for retry or failed +{"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.428Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.460Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-stale"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.819Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.186Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.201Z info: Notification marked as completed {"id":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.294Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationScheduler › ScheduledNotificationRepository › persists successful provider delivery with sanitized +response metadata + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   56 | +  57 | describe('ScheduledNotificationRepository', () => { + > 58 | test('persists successful provider delivery with sanitized +response metadata', async () => { +  | ^ +  59 | const id = await repository.create({ +  60 | payload: { message: 'Receipt test' }, +  61 | notificationType: +NotificationType.WEBHOOK, + + at src/tests/notification-scheduler.test.ts:58:5 + at src/tests/notification-scheduler.test.ts:57:3 + at Object. (src/tests/notification-scheduler.test.ts:16:1) + + ● NotificationScheduler › ScheduledNotificationRepository › persists a failed receipt with timeout details when +provider transport throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | }); +  107 | + > 108 | test('persists a failed receipt with timeout details when +provider transport throws', async () => { +  | ^ +  109 | const id = await repository.create({ +  110 | payload: { message: 'Timeout receipt test' }, +  111 | notificationType: +NotificationType.WEBHOOK, + + at src/tests/notification-scheduler.test.ts:108:5 + at src/tests/notification-scheduler.test.ts:57:3 + at Object. (src/tests/notification-scheduler.test.ts:16:1) + + ● NotificationScheduler › ScheduledNotificationRepository › does not deliver a notification that expires between +retries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   196 | }); +  197 | + > 198 | test('does not deliver a notification that expires between +retries', async () => { +  | ^ +  199 | const id = await repository.create({ +  200 | payload: { event: {}, contractConfig: +{}, message: 'Retry expiration' }, +  201 | notificationType: +NotificationType.DISCORD, + + at src/tests/notification-scheduler.test.ts:198:5 + at src/tests/notification-scheduler.test.ts:57:3 + at Object. (src/tests/notification-scheduler.test.ts:16:1) + +PASS src/services/indexing-reconciliation-engine.test.ts (40.234 s) +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts (71.802 s) + ● Console + + console.log + 2026-10-16T23:32:52.139Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.814Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.049Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.120Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.112Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.178Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.290Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:54.379Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:55.848Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.011Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.272Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.342Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:57.206Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:57.274Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:57.354Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:57.413Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/rpc-rate-limiter.test.ts (50.72 s) + ● Console + + console.log + 2026-10-16T23:32:54.094Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:55.494Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:55.836Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:55.998Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.118Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.157Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.174Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.229Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.318Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:56.429Z warn: RPC request throttled +{"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/store/event-registry.test.ts (30.311 s) +PASS src/services/notification-stats-cache.test.ts (56.028 s) +PASS src/api/cors-validator.test.ts (12.879 s) +PASS src/services/subscription-service.test.ts (16.569 s) +FAIL src/api/queue-visibility.test.ts (98.727 s) + ● Console + + console.log + 2026-10-16T23:31:51.133Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.627Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.661Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:52.759Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:53.538Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:54.313Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:54.362Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.021Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:55.112Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.278Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.791Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.823Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:57.998Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:31:58.396Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:00.131Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:00.269Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:00.459Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:10.250Z info: Handling GET /api/schedule/queue +{"requestId":"c8543402","correlationId":"11945a75-8cd8-4cbc-a1d8-e481619d0e7d","count":0,"durationMs":7626} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:15.573Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:15.869Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:15.939Z error: Database exec error {"sql":"-- Scheduled Notifications Database +Schema\r\n-- SQLite Database Schema for storing and tracking scheduled notifications\r\n\r\n-- Main table for +scheduled notifications\r\nCREATE TABLE IF NOT EXISTS scheduled_notifications (\r\n id INTEGER PRIMARY KEY +AUTOINCREMENT,\r\n \r\n -- Notification content and metadata\r\n payload TEXT NOT NULL, -- JSON +payload of the notification (compressed when large)\r\n payload_hash TEXT, -- HMAC hash of the +raw JSON payload for integrity checks\r\n notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN +('discord', 'email', 'webhook', 'sms')),\r\n target_recipient TEXT NOT NULL, -- User ID, webhook URL, or +recipient identifier\r\n \r\n -- Scheduling information\r\n execute_at DATETIME NOT NULL, -- When the +notification should be sent\r\n expires_at DATETIME, -- Optional deadline after which delivery +is skipped\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n updated_at DATETIME NOT NULL DEFAULT +CURRENT_TIMESTAMP,\r\n \r\n -- Status tracking\r\n status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK (status IN +('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'CANCELLED', 'EXPIRED', 'DEAD_LETTERED')),\r\n retry_count INTEGER +NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n max_retries INTEGER NOT NULL DEFAULT 3 CHECK (max_retries >= 0),\r\n + \r\n -- Processing metadata\r\n processing_started_at DATETIME,\r\n processing_completed_at DATETIME,\r\n +processor_id VARCHAR(100), -- Identifier of the worker processing this job\r\n lock_expires_at +DATETIME, -- Distributed lock expiration for race condition prevention\r\n \r\n -- Error +tracking\r\n last_error TEXT,\r\n error_details TEXT, -- JSON with full error context\r\n +\r\n -- Additional metadata\r\n event_id TEXT, -- Reference to the original event (if +applicable)\r\n contract_address TEXT, -- Stellar contract address (if applicable)\r\n priority +INTEGER NOT NULL DEFAULT 5 CHECK (priority BETWEEN 1 AND 10),\r\n metadata TEXT, -- +Additional JSON metadata\r\n next_retry_at DATETIME, -- When the next retry should be attempted\r\n + deduplication_key TEXT -- Caller-supplied key; duplicate inserts with the same key are silently +skipped\r\n);\r\n\r\n-- Indexes for performance optimization\r\nCREATE INDEX IF NOT EXISTS +idx_scheduled_notifications_status \r\n ON scheduled_notifications(status);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_scheduled_notifications_status_execute_at \r\n ON scheduled_notifications(status, execute_at);\r\n\r\nCREATE +INDEX IF NOT EXISTS idx_scheduled_notifications_lock_expires \r\n ON scheduled_notifications(lock_expires_at, +status);\r\n\r\n-- Migration: add next_retry_at for explicit retry scheduling\r\n\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_scheduled_notifications_next_retry_at\r\n ON scheduled_notifications(next_retry_at, status)\r\n WHERE status = +'PENDING';\r\n\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_created_at \r\n ON +scheduled_notifications(created_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_event_id \r\n ON +scheduled_notifications(event_id);\r\n\r\nCREATE UNIQUE INDEX IF NOT EXISTS idx_scheduled_notifications_dedup_key\r\n +ON scheduled_notifications(deduplication_key)\r\n WHERE deduplication_key IS NOT NULL;\r\n\r\nCREATE INDEX IF NOT +EXISTS idx_scheduled_notifications_target \r\n ON scheduled_notifications(target_recipient, status);\r\n\r\n-- +Migration: add next_retry_at for explicit retry scheduling (no-op when column exists in CREATE TABLE)\r\n-- SQLite +does not support IF NOT EXISTS for ADD COLUMN; runMigrations tolerates duplicate-column errors.\r\n-- Dead-letter +queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue (\r\n id INTEGER PRIMARY +KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n notification_type VARCHAR(50) NOT NULL +CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n target_recipient TEXT NOT NULL,\r\n payload +TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details TEXT,\r\n created_at DATETIME NOT NULL DEFAULT +CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= +0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES scheduled_notifications(id) ON DELETE +CASCADE\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_dead_letter_queue_created_at\r\n ON +dead_letter_queue(created_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_dead_letter_queue_notification_type\r\n ON +dead_letter_queue(notification_type);\r\n\r\n-- Notification execution history for auditing\r\nCREATE TABLE IF NOT +EXISTS notification_execution_log (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER +NOT NULL,\r\n execution_attempt INTEGER NOT NULL CHECK (execution_attempt > 0),\r\n execution_time DATETIME NOT NULL +DEFAULT CURRENT_TIMESTAMP,\r\n status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY', +'DEAD_LETTERED')),\r\n error_message TEXT,\r\n response_data TEXT, -- JSON response from +notification service\r\n duration_ms INTEGER CHECK (duration_ms IS NULL OR duration_ms >= 0),\r\n \r\n FOREIGN KEY +(scheduled_notification_id) REFERENCES scheduled_notifications(id) ON DELETE CASCADE\r\n);\r\n\r\nCREATE INDEX IF NOT +EXISTS idx_execution_log_notification_id \r\n ON notification_execution_log(scheduled_notification_id);\r\n\r\nCREATE +INDEX IF NOT EXISTS idx_execution_log_execution_time \r\n ON +notification_execution_log(execution_time);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_execution_log_status_execution_time +\r\n ON notification_execution_log(status, execution_time);\r\n\r\n-- One immutable receipt per provider delivery +attempt\r\nCREATE TABLE IF NOT EXISTS delivery_receipts (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n +notification_id INTEGER NOT NULL,\r\n channel TEXT NOT NULL,\r\n status TEXT NOT NULL CHECK (status IN ('delivered', +'failed', 'rejected', 'pending')),\r\n attempt_count INTEGER NOT NULL CHECK (attempt_count > 0),\r\n +provider_message_id TEXT,\r\n provider_response TEXT,\r\n error_code TEXT,\r\n error_message TEXT,\r\n created_at +DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n updated_at DATETIME NOT NULL DEFAULT +CURRENT_TIMESTAMP\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_delivery_receipts_notification_attempt\r\n ON +delivery_receipts(notification_id, attempt_count, id);\r\nCREATE INDEX IF NOT EXISTS +idx_delivery_receipts_status_created\r\n ON delivery_receipts(status, created_at);\r\n\r\n-- Migration: add +next_retry_at for explicit retry scheduling (no-op when column exists in CREATE TABLE)\r\n-- SQLite does not support +IF NOT EXISTS for ADD COLUMN; runMigrations tolerates duplicate-column errors.\r\n-- Trigger to update updated_at +timestamp\r\nCREATE TRIGGER IF NOT EXISTS update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON +scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE scheduled_notifications \r\n SET updated_at = +CURRENT_TIMESTAMP \r\n WHERE id = NEW.id;\r\nEND;\r\n\r\n-- Rate limit events table for auditing\r\nCREATE TABLE IF +NOT EXISTS rate_limit_events (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n client_id TEXT NOT NULL, + -- IP address or API key\r\n client_type VARCHAR(20) NOT NULL CHECK (client_type IN ('IP', 'API_KEY')),\r\n +endpoint TEXT NOT NULL, -- Request path/method\r\n method VARCHAR(10) NOT NULL, -- +Request method\r\n timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n limit_threshold INTEGER NOT NULL CHECK +(limit_threshold > 0),\r\n window_ms INTEGER NOT NULL CHECK (window_ms > 0)\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_rate_limit_events_timestamp \r\n ON rate_limit_events(timestamp);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_rate_limit_events_client_id \r\n ON rate_limit_events(client_id);\r\n\r\n-- Notification templates\r\nCREATE +TABLE IF NOT EXISTS notification_templates (\r\n id TEXT PRIMARY KEY,\r\n name TEXT NOT NULL,\r\n type TEXT NOT +NULL,\r\n subject TEXT,\r\n body TEXT NOT NULL,\r\n variables TEXT,\r\n metadata TEXT,\r\n created_at DATETIME +NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\r\n);\r\n\r\nCREATE +INDEX IF NOT EXISTS idx_notification_templates_type\r\n ON notification_templates(type);\r\n\r\nCREATE TRIGGER IF NOT +EXISTS update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n +UPDATE notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = NEW.id;\r\nEND;\r\n\r\n-- +Immutable audit trail for template modifications\r\nCREATE TABLE IF NOT EXISTS notification_template_audit_log (\r\n +id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n template_id TEXT NOT NULL,\r\n actor TEXT NOT NULL,\r\n action TEXT NOT +NULL DEFAULT 'UPDATE' CHECK (action IN ('UPDATE')),\r\n changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n +previous_snapshot TEXT NOT NULL,\r\n new_snapshot TEXT NOT NULL,\r\n FOREIGN KEY (template_id) REFERENCES +notification_templates(id) ON DELETE RESTRICT\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_template_audit_template_id\r\n ON notification_template_audit_log(template_id);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_template_audit_changed_at\r\n ON notification_template_audit_log(changed_at);\r\n\r\nCREATE TRIGGER IF NOT EXISTS +prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable');\r\nEND;\r\n\r\nCREATE TRIGGER IF NOT EXISTS +prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT +RAISE(ABORT, 'Audit records are immutable');\r\nEND;\r\n\r\n-- Event processing deduplication table - tracks processed +events to prevent duplicates during reorgs\r\n-- This table ensures idempotent processing of events even after +blockchain reorganizations\r\nCREATE TABLE IF NOT EXISTS processed_events (\r\n id INTEGER PRIMARY KEY +AUTOINCREMENT,\r\n \r\n -- Event identification (fingerprint components)\r\n event_id TEXT NOT NULL, + -- Unique identifier from blockchain RPC\r\n contract_address TEXT NOT NULL, -- Contract that emitted +the event\r\n fingerprint TEXT NOT NULL UNIQUE, -- Composite key: contract_address:event_id (for faster +lookups)\r\n \r\n -- Processing metadata\r\n ledger_number INTEGER NOT NULL CHECK (ledger_number >= 0),\r\n +tx_hash TEXT, -- Transaction hash (if available)\r\n event_type VARCHAR(50) NOT NULL, + -- Type from RPC (contract, system, etc)\r\n processed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n \r\n + -- Reorg detection and tracking\r\n is_reorg_duplicate BOOLEAN NOT NULL DEFAULT 0 CHECK (is_reorg_duplicate IN (0, +1)),\r\n reorg_detection_count INTEGER NOT NULL DEFAULT 0 CHECK (reorg_detection_count >= 0),\r\n last_redetected_at +DATETIME, -- When the event was last detected again (for reorg monitoring)\r\n \r\n -- Status and +metadata\r\n status VARCHAR(20) NOT NULL DEFAULT 'PROCESSED' CHECK (status IN ('PROCESSED', 'SKIPPED', 'ERROR')),\r\n + notification_sent BOOLEAN NOT NULL DEFAULT 0 CHECK (notification_sent IN (0, 1)),\r\n error_reason TEXT + -- If status is ERROR, what went wrong\r\n);\r\n\r\n-- Indexes for efficient lookups\r\nCREATE INDEX IF NOT +EXISTS idx_processed_events_fingerprint \r\n ON processed_events(fingerprint);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_processed_events_contract_event \r\n ON processed_events(contract_address, event_id);\r\n\r\nCREATE INDEX IF NOT +EXISTS idx_processed_events_processed_at \r\n ON processed_events(processed_at);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_processed_events_reorg_duplicates \r\n ON processed_events(is_reorg_duplicate, processed_at) \r\n WHERE +is_reorg_duplicate = 1;\r\n\r\nCREATE INDEX IF NOT EXISTS idx_processed_events_ledger_contract \r\n ON +processed_events(ledger_number, contract_address);\r\n\r\n-- Cursor tracking for event polling to detect +reorgs\r\nCREATE TABLE IF NOT EXISTS polling_cursors (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n \r\n -- +Contract tracking\r\n contract_address TEXT NOT NULL UNIQUE, -- Which contract this cursor is for\r\n \r\n -- +Cursor information\r\n cursor TEXT NOT NULL, -- Last known cursor from RPC\r\n ledger_number +INTEGER NOT NULL CHECK (ledger_number >= 0),\r\n updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n \r\n +-- Reorg detection\r\n reorg_detected BOOLEAN NOT NULL DEFAULT 0 CHECK (reorg_detected IN (0, 1)),\r\n +reorg_detection_count INTEGER NOT NULL DEFAULT 0 CHECK (reorg_detection_count >= 0)\r\n);\r\n\r\nCREATE INDEX IF NOT +EXISTS idx_polling_cursors_contract \r\n ON polling_cursors(contract_address);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_polling_cursors_updated_at\r\n ON polling_cursors(updated_at);\r\n\r\n-- Idempotency keys table for request +deduplication\r\nCREATE TABLE IF NOT EXISTS idempotency_keys (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n\r\n -- +Key identification\r\n idempotency_key TEXT NOT NULL UNIQUE, -- Client-provided idempotency key\r\n\r\n -- +Request and response tracking\r\n request_hash TEXT NOT NULL, -- Hash of request body for +validation\r\n response_notification_id INTEGER NOT NULL, -- ID of the created notification\r\n response_data TEXT +NOT NULL, -- JSON response to return on duplicate\r\n\r\n -- Lifecycle management\r\n created_at +DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n expires_at DATETIME NOT NULL, -- When this key should +be purged\r\n\r\n -- Status tracking\r\n status VARCHAR(20) NOT NULL DEFAULT 'PROCESSED' CHECK (status IN +('PROCESSED', 'EXPIRED')),\r\n\r\n FOREIGN KEY (response_notification_id) REFERENCES scheduled_notifications(id) ON +DELETE CASCADE\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_idempotency_keys_key\r\n ON +idempotency_keys(idempotency_key);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_idempotency_keys_expires_at\r\n ON +idempotency_keys(expires_at);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_idempotency_keys_created_at\r\n ON +idempotency_keys(created_at);\r\n\r\n-- Backpressure events table for tracking queue saturation and recovery\r\nCREATE +TABLE IF NOT EXISTS backpressure_events (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n\r\n -- Event tracking\r\n +event_type VARCHAR(20) NOT NULL CHECK (event_type IN ('ACTIVATED', 'DEACTIVATED')),\r\n queue_size INTEGER NOT NULL +CHECK (queue_size >= 0),\r\n target_throughput_per_sec INTEGER NOT NULL CHECK (target_throughput_per_sec >= +0),\r\n\r\n -- Duration tracking (for deactivation events)\r\n duration_ms INTEGER CHECK (duration_ms IS NULL OR +duration_ms >= 0),\r\n\r\n -- Additional metadata\r\n reason TEXT, -- Optional +reason/context for the event\r\n timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\r\n);\r\n\r\nCREATE INDEX IF +NOT EXISTS idx_backpressure_events_type\r\n ON backpressure_events(event_type);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_backpressure_events_timestamp\r\n ON backpressure_events(timestamp);\r\n\r\nCREATE INDEX IF NOT EXISTS +idx_backpressure_events_type_timestamp\r\n ON backpressure_events(event_type, timestamp);\r\n\r\n-- Persisted +notification delivery metrics snapshots for historical analytics\r\nCREATE TABLE IF NOT EXISTS +notification_metrics_snapshots (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n captured_at DATETIME NOT NULL DEFAULT +CURRENT_TIMESTAMP,\r\n window_start INTEGER NOT NULL,\r\n window_end INTEGER NOT NULL,\r\n total_recorded INTEGER +NOT NULL CHECK (total_recorded >= 0),\r\n snapshot_json TEXT NOT NULL,\r\n CHECK (window_start <= +window_end)\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_metrics_snapshots_captured_at\r\n ON +notification_metrics_snapshots(captured_at);\r\n-- Archived notification rows retain only terminal delivery +states\r\nCREATE TABLE IF NOT EXISTS notification_archive (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n +original_id INTEGER NOT NULL,\r\n payload TEXT NOT NULL,\r\n notification_type VARCHAR(50) NOT NULL CHECK +(notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n target_recipient TEXT NOT NULL,\r\n execute_at +DATETIME NOT NULL,\r\n expires_at DATETIME,\r\n created_at DATETIME NOT NULL,\r\n processing_completed_at +DATETIME,\r\n status VARCHAR(20) NOT NULL CHECK (status IN ('COMPLETED', 'FAILED', 'CANCELLED', 'EXPIRED', +'DEAD_LETTERED')),\r\n retry_count INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n last_error TEXT,\r\n +event_id TEXT,\r\n contract_address TEXT,\r\n metadata TEXT,\r\n archived_at DATETIME NOT NULL DEFAULT +CURRENT_TIMESTAMP\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_archive_original_id ON +notification_archive(original_id);\r\nCREATE INDEX IF NOT EXISTS idx_archive_archived_at ON +notification_archive(archived_at);\r\nCREATE INDEX IF NOT EXISTS idx_archive_status ON +notification_archive(status);\r\nCREATE INDEX IF NOT EXISTS idx_archive_contract_address ON +notification_archive(contract_address)\r\n WHERE contract_address IS NOT NULL;\r\nCREATE INDEX IF NOT EXISTS +idx_archive_event_id ON notification_archive(event_id)\r\n WHERE event_id IS NOT NULL;\r\n\r\n-- +===============================================\r\n-- QUERY PERFORMANCE INDEXES (migration 002)\r\n-- See +docs/DATABASE_QUERY_PERFORMANCE.md\r\n-- ===============================================\r\n\r\n-- Scheduler claim: +status + priority + execute_at for pending jobs\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_claim\r\n +ON scheduled_notifications(status, priority, execute_at)\r\n WHERE status = 'PENDING';\r\n\r\n-- Post-claim fetch by +processor lock\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_processor_lock\r\n ON +scheduled_notifications(processor_id, status, lock_expires_at);\r\n\r\n-- Search: status filter + created_at +sort\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_status_created\r\n ON scheduled_notifications(status, +created_at);\r\n\r\n-- Search: notification type / channel filter\r\nCREATE INDEX IF NOT EXISTS +idx_scheduled_notifications_type_status\r\n ON scheduled_notifications(notification_type, status);\r\n\r\n-- Contract +address lookups\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_contract\r\n ON +scheduled_notifications(contract_address)\r\n WHERE contract_address IS NOT NULL;\r\n\r\n-- Processed events: status ++ time range\r\nCREATE INDEX IF NOT EXISTS idx_processed_events_status_processed\r\n ON processed_events(status, +processed_at);\r\n\r\n-- Processed events: type filter\r\nCREATE INDEX IF NOT EXISTS +idx_processed_events_event_type_status\r\n ON processed_events(event_type, status);\r\n\r\n-- Processed events: tx +hash search\r\nCREATE INDEX IF NOT EXISTS idx_processed_events_tx_hash\r\n ON processed_events(tx_hash)\r\n WHERE +tx_hash IS NOT NULL;\r\n\r\n-- Execution log join for metrics (latest attempt per notification)\r\nCREATE INDEX IF NOT +EXISTS idx_execution_log_notification_attempt\r\n ON notification_execution_log(scheduled_notification_id, +execution_attempt);\r\n\r\n-- Rate-limit audit by client + time\r\nCREATE INDEX IF NOT EXISTS +idx_rate_limit_events_client_timestamp\r\n ON rate_limit_events(client_id, timestamp);\r\n\r\n-- +===============================================\r\n-- EVENT & NOTIFICATION QUERY INDEXES (migration 003)\r\n-- See +docs/DATABASE_QUERY_PERFORMANCE.md\r\n-- ===============================================\r\n\r\n-- Archival / +retention cleanup: terminal notifications ordered by completion time\r\nCREATE INDEX IF NOT EXISTS +idx_scheduled_notifications_archivable\r\n ON scheduled_notifications(processing_completed_at)\r\n WHERE status IN +('COMPLETED','FAILED','CANCELLED');\r\n\r\n-- Notification search: case-insensitive type filter + created_at +sort\r\nCREATE INDEX IF NOT EXISTS idx_scheduled_notifications_type_lower_created\r\n ON +scheduled_notifications(LOWER(notification_type), created_at);\r\n\r\n-- Processed-event search: case-insensitive type +filter + processed_at sort\r\nCREATE INDEX IF NOT EXISTS idx_processed_events_type_lower_processed\r\n ON +processed_events(LOWER(event_type), processed_at);\r\n","error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:16.002Z warn: Database connection failure detected; attempting recovery +{"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibi +lity.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:16.413Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:16.473Z warn: Database connection recovered {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:17.247Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:17.418Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:17.498Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:18.091Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:18.613Z info: Handling GET /api/schedule/queue +{"requestId":"25c55caf","correlationId":"8cc69c63-4285-4df2-88f4-2a011241b179","count":1,"durationMs":55} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:21.922Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:27.774Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:33.080Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:33.902Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:34.002Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:34.170Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:34.758Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:34.779Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:35.261Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:37.533Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:37.757Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:38.377Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:38.386Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:42.503Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:42.965Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.011Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:43.045Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.578Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.979Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.984Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:47.986Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:52.843Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.051Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.155Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:53.216Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:57.995Z info: Connected to SQLite database {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:58.291Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:58.323Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:32:58.394Z info: Database initialized successfully {"path":"C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns an empty list when no +notifications are pending + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   166 | +  167 | describe('GET /api/schedule/queue — pending jobs', () => { + > 168 | it('returns an empty list when no notifications are +pending', async () => { +  | ^ +  169 | const { status, body } = await +makeRequest(server, 'GET', '/api/schedule/queue'); +  170 | +  171 | expect(status).toBe(200); + + at src/api/queue-visibility.test.ts:168:7 + at src/api/queue-visibility.test.ts:167:11 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns an empty list when no +notifications are pending + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   96 | }); +  97 | + > 98 | afterEach(async () => { +  | ^ +  99 | await new +Promise<void>((resolve, reject) => +  100 | server.close((err) => (err ? reject(err) : +resolve())), +  101 | ); + + at src/api/queue-visibility.test.ts:98:12 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › does not return completed or failed +notifications + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › does not return completed or failed +notifications + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › respects the ?limit= query parameter + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › respects the ?limit= query parameter + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › respects the ?limit= query parameter + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns 503 when the scheduler is not +enabled + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns 503 when the scheduler is not +enabled + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns 503 when the scheduler is not +enabled + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › is accessible via the /api/v1/ +version alias + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › is accessible via the /api/v1/ +version alias + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › is accessible via the /api/v1/ +version alias + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + Server is not running. + +PASS src/api/content-type.test.ts (22.231 s) +PASS src/services/notification-template-repository.test.ts (11.921 s) + ● Console + + console.log + 2026-10-16T23:33:07.709Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:07.922Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:07.932Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:07.943Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.007Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.026Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.030Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.271Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.274Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.276Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.665Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.674Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.701Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.702Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.703Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.706Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.728Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.881Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.883Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.884Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:08.894Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.248Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.512Z info: Notification template updated +{"templateId":"tmpl-001","actor":"admin@example.com"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.547Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.565Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.625Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.626Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.627Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.629Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.649Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.653Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.657Z info: Template audit record created +{"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.660Z info: Notification template updated +{"templateId":"tmpl-001","actor":"editor-2"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.669Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.672Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.718Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.720Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.721Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.723Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.727Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.729Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.738Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.739Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.741Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.750Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.754Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.756Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.767Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.769Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.770Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:09.778Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.287Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.293Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.325Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.327Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.328Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.366Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.427Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.489Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.494Z info: [TemplateCache] Template invalidated +{"templateId":"tmpl-001","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.498Z info: Notification template updated +{"templateId":"tmpl-001","actor":"cache-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.502Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.551Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.580Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.582Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.584Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.594Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.605Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.613Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:10.810Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET +actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.122Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.142Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.164Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.166Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.169Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.172Z info: Notification template created {"templateId":"tmpl-001"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.174Z info: Template audit record created +{"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.177Z info: Notification template updated +{"templateId":"tmpl-001","actor":"auditor"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.181Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log +WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.183Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.188Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.207Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.211Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.213Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.232Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.237Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.402Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.405Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.407Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:11.429Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +ts-jest[ts-compiler] (WARN) src/services/providers/provider-health.test.ts:15:56 - +error TS2345: Argument of type '{}' is not assignable to parameter of type 'DiscordConfig'. + Type '{}' is missing the following properties from type 'DiscordConfig': webhookUrl, webhookId + +15 const provider = new DiscordNotificationProvider({}); +   ~~ +src/services/providers/provider-health.test.ts:26:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +26 const provider = new DiscordNotificationProvider({ +   ~ +27 webhookUrl: 'https://discord.com/api/webhooks/123/my-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +28 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +src/services/providers/provider-health.test.ts:42:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +42 const provider = new DiscordNotificationProvider({ +   ~ +43 webhookUrl: 'https://discord.com/api/webhooks/123456789/my-super-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +44 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +PASS src/scripts/replay-event.test.ts (20.257 s) + ● Console + + console.log + 2026-10-16T23:33:12.025Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:12.973Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:13.421Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:13.659Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:14.006Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:14.170Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:14.486Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:14.564Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:14.667Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:14.812Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:15.035Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:15.424Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:15.483Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:15.537Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:15.746Z info: Event processed and recorded {"eventId":"duplicate-test-event","contract +Address":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a0 +26ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:16.200Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:16.247Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:16.444Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:16.557Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:16.738Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:16.905Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAd +dress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de +1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.081Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.271Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.445Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.448Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.452Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.557Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.567Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.598Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.804Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:17.918Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:18.008Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:18.016Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:19.549Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:19.882Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:19.890Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:19.908Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load.test.ts (23.083 s) + ● Console + + console.log + + === Load Test Results (100 events) === + + at Object. (src/__tests__/load.test.ts:62:15) + + console.log + Total processing time: 1531ms + + at Object. (src/__tests__/load.test.ts:63:15) + + console.log + Throughput: 65.32 events/second + + at Object. (src/__tests__/load.test.ts:64:15) + + console.log + Average latency per event: 15.31ms + + at Object. (src/__tests__/load.test.ts:65:15) + + console.log + + === Load Test Results (500 events, maxConcurrency=20) === + + at Object. (src/__tests__/load.test.ts:109:15) + + console.log + Total processing time: 3224ms + + at Object. (src/__tests__/load.test.ts:110:15) + + console.log + Throughput: 155.09 events/second + + at Object. (src/__tests__/load.test.ts:111:15) + + console.log + Average latency per event: 6.45ms + + at Object. (src/__tests__/load.test.ts:112:15) + +PASS src/services/providers/provider-health.test.ts (22.468 s) + ● Console + + console.log + 2026-10-16T23:33:21.882Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/database-cleanup-job.test.ts (25.032 s) + ● Console + + console.log + 2026-10-16T23:33:20.156Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:23.125Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:23.292Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:23.453Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/events-server.history.test.ts +PASS src/api/events-server.health.test.ts +PASS src/services/template-service.test.ts + ● Console + + console.log + 2026-10-16T23:33:24.996Z info: Template created successfully +{"templateId":1,"uniqueKey":"welcome-email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:25.325Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome +{{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:25.379Z error: Failed to create template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" +","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:25.396Z error: Failed to create template {"error":{"message":"channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be +one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at +validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:25.783Z error: Failed to create template {"error":{"message":"description: must be a +string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","description":12345}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:25.895Z error: Failed to create template {"error":{"message":"name: must not exceed +255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTe +mplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:26.122Z error: Failed to create template {"error":{"message":"variables: must be an +array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","variables":[1,2,3]}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:26.256Z error: Failed to create template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n +at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\te +stWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome +Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome +aboard!","defaultValues":"nope"}} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:26.409Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:27.142Z error: Failed to update template {"error":{"message":"name: is required and +must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a +non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:27.269Z info: Template updated successfully {"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:28.124Z error: Failed to update template {"error":{"message":"defaultValues: must be +an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at +InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at +TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n + at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise +()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at +processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at +_runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at +_runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at +runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW +OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker +(C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry +941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/load-test-runner.test.ts (21.089 s) +PASS src/api/notifications-history.test.ts (24.096 s) +ts-jest[ts-compiler] (WARN) src/utils/logger.test.ts:191:27 - error TS2345: +Argument of type '(_info: unknown, next: () => void) => void' is not assignable to parameter of type '(...args: +unknown[]) => any'. + Types of parameters 'next' and 'args' are incompatible. + Type 'unknown' is not assignable to type '() => void'. + +191 .mockImplementation((_info: unknown, next: () => void) => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/utils/logger.test.ts:310:27 - error TS2345: Argument of type '(_info: +unknown, next: () => void) => void' is not assignable to parameter of type '(...args: unknown[]) => any'. + Types of parameters 'next' and 'args' are incompatible. + Type 'unknown' is not assignable to type '() => void'. + +310 .mockImplementation((_info: unknown, next: () => void) => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PASS src/services/event-subscriber-checkpoint.test.ts (27.039 s) +FAIL src/__tests__/stress.test.ts (341.047 s) + ● Console + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 1,000 Concurrent Events + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 1000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 1656ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 603.86 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 134.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 19.16ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 13.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 67.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 87.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 303.83 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 309.87 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 6.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 34.47 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 29.10 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -5.37 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 5,000 Events Sustained Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 5000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 12195ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 410.00 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 306.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 43.89ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 30.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 134.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 303.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 312.50 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 323.25 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 10.75 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 40.45 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 20.28 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -20.17 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + Progress: 600/10000 (6.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 1400/10000 (14.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 2200/10000 (22.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 3200/10000 (32.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 4300/10000 (43.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 5100/10000 (51.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + Progress: 6500/10000 (65.00%) + + at Timeout._onTimeout (src/__tests__/stress.test.ts:296:17) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: 10,000 Events - System Stability + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 10000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 39692ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 251.94 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 2578.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 78.78ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 17.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 282.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 1086.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 325.50 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 330.03 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 4.54 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 32.18 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 34.95 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 2.77 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + Sending first burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:356:15) + + console.log + Waiting idle period (2 seconds)... + + at Object. (src/__tests__/stress.test.ts:373:15) + + console.log + Sending second burst of 2000 events... + + at Object. (src/__tests__/stress.test.ts:377:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Burst Traffic (2x2000 events) + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 4000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 4556ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 877.96 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 88.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 12.25ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 9.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 30.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 87.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 333.15 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 356.97 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 23.82 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 35.76 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 45.17 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 9.41 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + First burst time: 1004ms + + at Object. (src/__tests__/stress.test.ts:406:15) + + console.log + Second burst time: 1509ms + + at Object. (src/__tests__/stress.test.ts:407:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Priority Queue Under Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 2000 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 3458ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 578.37 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 0.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 434.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 33.88ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 14.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 133.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 432.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 358.94 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 325.84 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: -33.10 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 48.34 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 78.04 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: 29.70 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Priority Analysis: + + at Object. (src/__tests__/stress.test.ts:492:15) + + console.log + High-priority in first 500: 500 / 500 (100.00%) + + at Object. (src/__tests__/stress.test.ts:493:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Deduplication Test (3000 events, 1500 unique) + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 3000 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 1500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 2764ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 1085.38 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 130.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 22.45ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 15.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 80.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 119.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 327.91 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 336.89 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 8.98 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 80.59 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 22.54 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -58.05 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Deduplication Efficiency: + + at Object. (src/__tests__/stress.test.ts:563:15) + + console.log + Total Enqueued: 3000 + + at Object. (src/__tests__/stress.test.ts:564:15) + + console.log + Unique Events: 1500 + + at Object. (src/__tests__/stress.test.ts:565:15) + + console.log + Actually Processed: 1500 + + at Object. (src/__tests__/stress.test.ts:566:15) + + console.log + Duplicates Blocked: 1500 + + at Object. (src/__tests__/stress.test.ts:567:15) + + console.log + + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + + console.log + STRESS TEST REPORT: Discord Notification Service Load + + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + + console.log + Events Processed: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + + console.log + Success Count: 500 + + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + + console.log + Failure Count: 0 + + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + + console.log + Total Time: 3302ms + + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + + console.log + Throughput: 151.42 events/second + + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + + console.log + + Latency Statistics: + + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + + console.log + Min: 1536.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + + console.log + Max: 3300.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + + console.log + Avg: 1887.31ms + + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + + console.log + P50 (Median): 1786.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + + console.log + P95: 2962.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + + console.log + P99: 2993.00ms + + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + + console.log + + Memory Usage: + + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + + console.log + Before - Heap: 339.03 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + + console.log + After - Heap: 354.02 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + + console.log + Delta - Heap: 14.98 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + + console.log + Before - RSS: 30.66 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + + console.log + After - RSS: 22.85 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + + console.log + Delta - RSS: -7.81 MB + + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + + console.log + ================================================================================ + + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + + console.log + + Discord Service Metrics: + + at Object. (src/__tests__/stress.test.ts:630:15) + + console.log + Total Sent: undefined + + at Object. (src/__tests__/stress.test.ts:631:15) + + console.log + Duplicates Skipped: undefined + + at Object. (src/__tests__/stress.test.ts:632:15) + + ● Stress Test Suite: Notification Processing › 1. High-Volume Concurrent Processing › should handle 5,000 events +with sustained throughput + + expect(received).toBeLessThan(expected) + + Expected: < 200 + Received: 303 + +   252 | expect(processedIds.length).toBe(eventCount); +  253 | expect(report.throughput).toBeGreaterThan(100); + > 254 | +expect(report.latency.p99).toBeLessThan(200); +  | ^ +  255 | }, 120000); +  256 | }); +  257 | + + at Object. (src/__tests__/stress.test.ts:254:34) + + ● Stress Test Suite: Notification Processing › 2. Sustained Load Over Time › should maintain stability processing +10,000 events + + expect(received).toBeLessThan(expected) + + Expected: < 50 + Received: 78.78 + +   319 | expect(processedIds.length).toBe(eventCount); +  320 | expect(report.throughput).toBeGreaterThan(100); + > 321 | +expect(report.latency.avg).toBeLessThan(50); +  | ^ +  322 | +  323 | // Check memory doesn't grow excessively (less than 200MB increase) +  324 | const memoryIncreaseMB = (memoryAfter.heapUsed - +memoryBefore.heapUsed) / (1024 * 1024); + + at Object. (src/__tests__/stress.test.ts:321:34) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + ================================================================================". + +   94 | +  95 | // Log to console for visibility + > 96 | console.log('\n' + +'='.repeat(80)); +  | ^ +  97 | console.log(`STRESS TEST REPORT: ${testName}`); +  98 | console.log('='.repeat(80)); +  99 | console.log(`Events Processed: ${eventCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:96:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "STRESS TEST REPORT: Error Recovery Under Load". + +   95 | // Log to console for visibility +  96 | console.log('\n' + +'='.repeat(80)); + > 97 | console.log(`STRESS TEST REPORT: +${testName}`); +  | ^ +  98 | console.log('='.repeat(80)); +  99 | console.log(`Events Processed: ${eventCount}`); +  100 | console.log(`Success Count: ${successCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:97:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================". + +   96 | console.log('\n' + +'='.repeat(80)); +  97 | console.log(`STRESS TEST REPORT: ${testName}`); + > 98 | +console.log('='.repeat(80)); +  | ^ +  99 | console.log(`Events Processed: ${eventCount}`); +  100 | console.log(`Success Count: ${successCount}`); +  101 | console.log(`Failure Count: ${failureCount}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:98:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Events Processed: 1000". + +   97 | console.log(`STRESS TEST REPORT: ${testName}`); +  98 | console.log('='.repeat(80)); + > 99 | console.log(`Events Processed: +${eventCount}`); +  | ^ +  100 | console.log(`Success Count: ${successCount}`); +  101 | console.log(`Failure Count: ${failureCount}`); +  102 | console.log(`Total Time: ${totalTimeMs}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:99:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Success Count: 900". + +   98 | console.log('='.repeat(80)); +  99 | console.log(`Events Processed: ${eventCount}`); + > 100 | console.log(`Success Count: +${successCount}`); +  | ^ +  101 | console.log(`Failure Count: ${failureCount}`); +  102 | console.log(`Total Time: ${totalTimeMs}ms`); +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:100:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Failure Count: 0". + +   99 | console.log(`Events Processed: ${eventCount}`); +  100 | console.log(`Success Count: ${successCount}`); + > 101 | console.log(`Failure Count: +${failureCount}`); +  | ^ +  102 | console.log(`Total Time: ${totalTimeMs}ms`); +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  104 | console.log(`\nLatency Statistics:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:101:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Total Time: 184480ms". + +   100 | console.log(`Success Count: ${successCount}`); +  101 | console.log(`Failure Count: ${failureCount}`); + > 102 | console.log(`Total Time: +${totalTimeMs}ms`); +  | ^ +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  104 | console.log(`\nLatency Statistics:`); +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:102:13) + at Object. (src/__tests__/stress.test.ts:698:22) + +PASS src/api/rate-limit-scenarios.test.ts (21.095 s) + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "Throughput: 5.42 events/second". + +   101 | console.log(`Failure Count: ${failureCount}`); +  102 | console.log(`Total Time: ${totalTimeMs}ms`); + > 103 | console.log(`Throughput: +${throughput.toFixed(2)} events/second`); +  | ^ +  104 | console.log(`\nLatency Statistics:`); +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:103:13) + at Object. (src/__tests__/stress.test.ts:698:22) + +PASS src/utils/logger.test.ts (29.75 s) + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Latency Statistics:". + +   102 | console.log(`Total Time: ${totalTimeMs}ms`); +  103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); + > 104 | console.log(`\nLatency Statistics:`); +  | ^ +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:104:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Min: 1.00ms". + +   103 | console.log(`Throughput: ${throughput.toFixed(2)} +events/second`); +  104 | console.log(`\nLatency Statistics:`); + > 105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  | ^ +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:105:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Max: 348.00ms". + +   104 | console.log(`\nLatency Statistics:`); +  105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); + > 106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  | ^ +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:106:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Avg: 43.51ms". + +   105 | console.log(` Min: +${minLatency.toFixed(2)}ms`); +  106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); + > 107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  | ^ +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); +  110 | console.log(` P99: ${p99.toFixed(2)}ms`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:107:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P50 (Median): 14.00ms". + +   106 | console.log(` Max: +${maxLatency.toFixed(2)}ms`); +  107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); + > 108 | console.log(` P50 (Median): +${p50.toFixed(2)}ms`); +  | ^ +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); +  110 | console.log(` P99: ${p99.toFixed(2)}ms`); +  111 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:108:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P95: 262.00ms". + +   107 | console.log(` Avg: +${avgLatency.toFixed(2)}ms`); +  108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); + > 109 | console.log(` P95: +${p95.toFixed(2)}ms`); +  | ^ +  110 | console.log(` P99: ${p99.toFixed(2)}ms`); +  111 | +  112 | if (memoryUsage) { + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:109:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " P99: 348.00ms". + +   108 | console.log(` P50 (Median): ${p50.toFixed(2)}ms`); +  109 | console.log(` P95: ${p95.toFixed(2)}ms`); + > 110 | console.log(` P99: +${p99.toFixed(2)}ms`); +  | ^ +  111 | +  112 | if (memoryUsage) { +  113 | console.log(`\nMemory Usage:`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:110:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Memory Usage:". + +   111 | +  112 | if (memoryUsage) { + > 113 | console.log(`\nMemory Usage:`); +  | ^ +  114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:113:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - Heap: 355.91 MB". + +   112 | if (memoryUsage) { +  113 | console.log(`\nMemory Usage:`); + > 114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  | ^ +  115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:114:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - Heap: 302.55 MB". + +   113 | console.log(`\nMemory Usage:`); +  114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); + > 115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  | ^ +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:115:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - Heap: -53.37 MB". + +   114 | console.log(` Before - Heap: +${report.memoryUsage!.before.heapUsed}`); +  115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); + > 116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  | ^ +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:116:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Before - RSS: 29.93 MB". + +   115 | console.log(` After - Heap: +${report.memoryUsage!.after.heapUsed}`); +  116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); + > 117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  | ^ +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  120 | } + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:117:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " After - RSS: 16.27 MB". + +   116 | console.log(` Delta - Heap: +${report.memoryUsage!.delta.heapUsed}`); +  117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); + > 118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); +  | ^ +  119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  120 | } +  121 | console.log('='.repeat(80) + +'\n'); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:118:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Delta - RSS: -13.66 MB". + +   117 | console.log(` Before - RSS: +${report.memoryUsage!.before.rss}`); +  118 | console.log(` After - RSS: +${report.memoryUsage!.after.rss}`); + > 119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  | ^ +  120 | } +  121 | console.log('='.repeat(80) + +'\n'); +  122 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:119:15) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log "================================================================================ + ". + +   119 | console.log(` Delta - RSS: +${report.memoryUsage!.delta.rss}`); +  120 | } + > 121 | console.log('='.repeat(80) ++ '\n'); +  | ^ +  122 | +  123 | return report; +  124 | }; + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at generateMetricsReport (src/__tests__/stress.test.ts:121:13) + at Object. (src/__tests__/stress.test.ts:698:22) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " + Error Recovery Statistics:". + +   707 | ); +  708 | + > 709 | console.log(`\nError Recovery +Statistics:`); +  | ^ +  710 | console.log(` Initial Failures: ${retryCount}`); +  711 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  712 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:709:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Initial Failures: 300". + +   708 | +  709 | console.log(`\nError Recovery Statistics:`); + > 710 | console.log(` Initial Failures: +${retryCount}`); +  | ^ +  711 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); +  712 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  713 | + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:710:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Successful Retries: 200". + +   709 | console.log(`\nError Recovery Statistics:`); +  710 | console.log(` Initial Failures: ${retryCount}`); + > 711 | console.log(` Successful Retries: ${processedIds.length +- (eventCount - retryCount)}`); +  | ^ +  712 | console.log(` Final Success Rate: ${((processedIds.length / eventCount) * +100).toFixed(2)}%`); +  713 | +  714 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:711:15) + + + ● Cannot log after tests are done. Did you forget to wait for something async in your test? + Attempted to log " Final Success Rate: 90.00%". + +   710 | console.log(` Initial Failures: ${retryCount}`); +  711 | console.log(` Successful Retries: ${processedIds.length - (eventCount - +retryCount)}`); + > 712 | console.log(` Final Success Rate: +${((processedIds.length / eventCount) * 100).toFixed(2)}%`); +  | ^ +  713 | +  714 | expect(processedIds.length).toBeGreaterThanOrEqual(eventCount +* 0.95); // At least 95% success +  715 | expect(retryCount).toBeGreaterThan(0); // Verify +retries occurred + + at console.log (node_modules/@jest/console/build/index.js:147:10) + at Object. (src/__tests__/stress.test.ts:712:15) + +PASS src/services/stellar-rpc-manager.test.ts (51.637 s) +PASS src/services/event-deduplication-service.test.ts (25.443 s) +PASS src/services/notification-template-service.test.ts (13.344 s) + ● Console + + console.log + 2026-10-16T23:33:53.702Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:54.391Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:54.489Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:54.509Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.400Z info: Notification template created {"templateId":"svc-template"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.500Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.885Z info: Template audit record created +{"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.916Z info: [TemplateCache] Template invalidated +{"templateId":"svc-template","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.919Z info: Notification template updated +{"templateId":"svc-template","actor":"service-admin"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.922Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.927Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:55.961Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.223Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.228Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.230Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.238Z info: Notification template created {"templateId":"tmpl-a"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.441Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.505Z info: Notification template created {"templateId":"tmpl-b"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:56.516Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.072Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.078Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.149Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.150Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.155Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.158Z info: Notification template created {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.161Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.173Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.176Z info: Notification template deleted {"templateId":"del-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.178Z info: [TemplateCache] Template invalidated +{"templateId":"del-tmpl","deleted":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.202Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.206Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.223Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.225Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.226Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.228Z info: Notification template created {"templateId":"render-tmpl"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.229Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.232Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.234Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.259Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.260Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.325Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.331Z info: Notification template created {"templateId":"render-missing"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.332Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.397Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.399Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.416Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.417Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.437Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.459Z info: Notification template created {"templateId":"no-vars"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.463Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:57.465Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/__tests__/api-contract.test.ts (22.321 s) + ● Console + + console.log + 2026-10-16T23:33:58.956Z info: Handling GET /api/events +{"requestId":"24ea4815","correlationId":"91b8cacc-af10-44f9-bfe6-e48d9a824bf3","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.058Z info: GET /api/events complete +{"requestId":"24ea4815","correlationId":"91b8cacc-af10-44f9-bfe6-e48d9a824bf3","returned":0,"durationMs":200} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.519Z warn: Unhandled request {"requestId":"5ae2eaf5","correlationId":"d41fcb83-5454 +-4fa2-951c-7429c5d9dea5","method":"GET","url":"/api/definitely-not-a-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.541Z info: Request completed +{"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":23,"requestId":"5ae2eaf5"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.863Z info: Notification analytics aggregator initialized with defaults + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.873Z info: Handling GET /api/analytics {"requestId":"fcfaedae","correlationId":"654 +0b86e-c9c2-4c42-8dbb-3561d02a77c1","totalRecorded":0,"reset":false,"durationMs":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.887Z info: Handling GET /api/events +{"requestId":"00cb5727","correlationId":"1d5a2034-27fd-4f9e-bfb6-ca96dbe4e9a6","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:33:59.889Z info: GET /api/events complete +{"requestId":"00cb5727","correlationId":"1d5a2034-27fd-4f9e-bfb6-ca96dbe4e9a6","returned":0,"durationMs":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/payload-validation.integration.test.ts (65.076 s) +PASS src/services/retry-policy.test.ts (39.003 s) +PASS src/utils/pagination.test.ts (12.297 s) +PASS src/store/preference-store.test.ts (24.274 s) +PASS src/utils/redact.test.ts (25.385 s) +PASS src/api/delivery-metrics-api.test.ts (44.866 s) + ● Console + + console.log + 2026-10-16T23:34:06.868Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"56276834" +,"correlationId":"402b6d34-76b1-4901-9c74-b9c9de332b7d","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":200} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.112Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"b0a777b4" +,"correlationId":"8f2466fb-77b0-4da7-8348-c5193177ce6a","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.310Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"de00293a" +,"correlationId":"579847d7-a319-4452-a8ed-e96ce623275d","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs" +:4} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.324Z info: Delivery metrics window reset after read +{"requestId":"de00293a","correlationId":"579847d7-a319-4452-a8ed-e96ce623275d"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.455Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"8d287055" +,"correlationId":"2de7870b-e98d-472b-bd2b-6fab020e6f0c","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":12} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.522Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"06909421" +,"correlationId":"f8f2dd01-4768-4c1c-9724-b2d27a5a8ca7","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs +":16} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.873Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"020697c0","correlationId":"187fa5cf-38e5-4ce0-b95f-3d7e3e07ba93","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:09.924Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"020697c0","correlationId":"187fa5cf-38e5-4ce0-b95f-3d7e3e07ba93","count":1,"durationMs":75} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:10.379Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"b0f6d5b4","correlationId":"4f1e7fab-0719-41f7-b973-2fc93fcf7547","limit":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:10.472Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"b0f6d5b4","correlationId":"4f1e7fab-0719-41f7-b973-2fc93fcf7547","count":0,"durationMs":119} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:10.700Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"1 +8b17e8d","correlationId":"370b9076-2806-4990-9aa4-6915d2e03f63","limit":50,"since":"2024-01-01T00:00:00.000Z"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:10.777Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"18b17e8d","correlationId":"370b9076-2806-4990-9aa4-6915d2e03f63","count":0,"durationMs":143} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:10.889Z info: Handling GET /api/notifications/delivery-metrics/history +{"requestId":"e57f95c4","correlationId":"b5a6b0f5-f84d-4ca0-a68a-be77b709a3ae","limit":50} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:10.942Z info: GET /api/notifications/delivery-metrics/history complete +{"requestId":"e57f95c4","correlationId":"b5a6b0f5-f84d-4ca0-a68a-be77b709a3ae","count":0,"durationMs":53} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/dns-rebinding-guard.test.ts (22.668 s) +PASS src/config-schema.test.ts (44.084 s) +ts-jest[ts-compiler] (WARN) src/api/__tests__/api-contract.test.ts:20:46 - +error TS2345: Argument of type '{ status: string; }' is not assignable to parameter of type 'never'. + +20 getHealth: jest.fn().mockResolvedValue({ status: 'healthy' }), +   ~~~~~~~~~~~~~~~~~~~~~ +src/api/__tests__/api-contract.test.ts:60:38 - error TS2345: Argument of +type '{ ok: number; }' is not assignable to parameter of type 'never'. + +60 get: jest.fn().mockResolvedValue({ ok: 1 }), +   ~~~~~~~~~ +PASS src/utils/ledger-range-validator.test.ts (14.622 s) +PASS src/services/scheduled-notification-repository-cache.test.ts (20.317 s) + ● Console + + console.log + 2026-10-16T23:34:19.200Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:19.895Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:19.924Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:19.935Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.067Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.080Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.093Z info: Notification marked as completed {"id":2} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.123Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.129Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.133Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.172Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.197Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.276Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.278Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.281Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.283Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.285Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + Performance comparison: + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:130:15) + + console.log + Uncached call: 2ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:131:15) + + console.log + Cached call: 0ms + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:132:15) + + console.log + Improvement: 100.0% + + at Object. (src/services/scheduled-notification-repository-cache.test.ts:133:15) + + console.log + 2026-10-16T23:34:20.297Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.322Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.324Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.326Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.327Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.331Z info: Notification marked as completed {"id":13} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.367Z info: Notification marked as completed {"id":14} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.385Z info: Notification marked as completed {"id":15} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:20.537Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/config-logging.test.ts (11.896 s) +PASS src/examples/webhook-consumer-verification-example.test.ts (8.591 s) +PASS src/services/backpressure-controller.test.ts (12.158 s) + ● Console + + console.log + 2026-10-16T23:34:23.874Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.081Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.085Z info: Backpressure deactivated: queue recovered +{"queueSize":49,"threshold":50,"durationMs":3,"targetThroughputPerSec":100} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.090Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.100Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.130Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.258Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.275Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:24.310Z warn: Backpressure activated: queue saturation detected +{"queueSize":101,"threshold":100,"targetThroughputPerSec":10} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/event-processing-queue.test.ts (28.963 s) +PASS src/services/event-quarantine.test.ts (7.195 s) +PASS src/services/notification-api.test.ts (21.129 s) + ● Console + + console.log + 2026-10-16T23:34:26.862Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:28.255Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:28.267Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:28.392Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:28.615Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:28.862Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/historical-backfill-service.test.ts (19.004 s) +PASS src/utils/request-id.test.ts (9.069 s) +PASS src/utils/metadata-validator.test.ts (19.06 s) +PASS src/services/dead-letter-manager.test.ts (8.929 s) +PASS src/services/subscription-event-emitter.test.ts (7.178 s) +PASS src/api/__tests__/api-contract.test.ts (59.996 s) +PASS src/services/search-suggestion.test.ts (10.587 s) + ● Console + + console.log + 2026-10-16T23:34:34.775Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:34.980Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:34.990Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:35.011Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:35.367Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/api/api-version-registry.test.ts (7.344 s) +PASS src/services/event-provenance.test.ts (8.541 s) + ● Console + + console.log + 2026-10-16T23:34:35.193Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:35.454Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:35.509Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:35.839Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:37.141Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-search-service.test.ts (10.106 s) + ● Console + + console.log + 2026-10-16T23:34:41.915Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:42.148Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:42.153Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:42.184Z info: Notification search complete +{"total":0,"returned":0,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:42.195Z info: Notification search complete +{"total":1,"returned":1,"limit":20,"offset":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/event-utils.test.ts (18.575 s) +PASS src/providers/provider-capabilities.test.ts (21.821 s) +PASS src/services/delivery-receipt-repository.test.ts (17.32 s) +PASS src/utils/payload-size-validator.test.ts (15.184 s) +PASS src/services/event-store-consistency.test.ts (25.064 s) + ● Console + + console.log + 2026-10-16T23:34:54.776Z info: Connected to SQLite database {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:55.759Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:55.801Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:55.861Z info: Database initialized successfully {"path":":memory:"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:34:56.612Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-health-monitor.test.ts (31.303 s) + ● Console + + console.log + 2026-10-16T23:35:00.626Z info: NotificationHealthMonitor started {"intervalMs":30000} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:01.113Z info: NotificationHealthMonitor stopped + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/logger-structured.test.ts (8.547 s) +PASS src/services/idempotency-key-service.test.ts (35.454 s) +PASS src/utils/validation.test.ts (44.824 s) +PASS src/__tests__/shutdown-unit.test.ts (10.69 s) +PASS src/utils/config-snapshot.test.ts +PASS src/services/job-monitor.test.ts (10.3 s) + ● Console + + console.log + 2026-10-16T23:35:06.729Z error: Job monitor: job failed +{"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":77} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-template-cache.test.ts + ● Console + + console.log + 2026-10-16T23:35:07.059Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.212Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.217Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.225Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.231Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.238Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.248Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.256Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.262Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.265Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.268Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.271Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.275Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.279Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.296Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.298Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.302Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.305Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.307Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.309Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.311Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.313Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.315Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.317Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.322Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.324Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.329Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.333Z info: [TemplateCache] All templates invalidated + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/retry-scheduler.test.ts (29.856 s) + ● Console + + console.log + 2026-10-16T23:35:07.301Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.325Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.329Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.332Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.362Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.444Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.459Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.471Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.497Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.513Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.524Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.536Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.545Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/utils/payload-compression.test.ts (33.513 s) + ● Console + + console.log + 2026-10-16T23:35:07.420Z info: Payload compressed +{"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.772Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:07.998Z warn: Payload decompression failed, returning original value +{"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/test-utils/notification-fixture-builder.test.ts (24.568 s) +PASS src/middleware/request-id.test.ts (5.215 s) +PASS src/utils/log-level-resolver.test.ts +PASS src/utils/batch-validator.test.ts (20.717 s) +PASS src/services/notification-metrics-runner.test.ts (19.407 s) +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/services/notification-import-service.test.ts (10.573 s) + ● Console + + console.log + 2026-10-16T23:35:12.288Z info: Bulk notification import complete +{"imported":2,"skipped":0,"total":2,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:12.472Z info: Bulk notification import complete +{"imported":1,"skipped":2,"total":3,"format":"json"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:12.498Z info: Bulk notification import complete +{"imported":1,"skipped":1,"total":2,"format":"csv"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/middleware/body-limit.test.ts (5.633 s) +ts-jest[ts-compiler] (WARN) src/services/event-normalization.test.ts:101:35 - +error TS2353: Object literal may only specify known properties, and 'payload_version' does not exist +in type 'ScMapEntry[]'. + +101 value: xdr.ScVal.scvMap({ payload_version: 1, notification_id: 'abc' }), +   ~~~~~~~~~~~~~~~ +PASS src/services/event-normalization.test.ts (7.183 s) +PASS src/utils/cors-validator.test.ts +PASS src/utils/notification-version.test.ts (5.172 s) +PASS src/middleware/security-headers.test.ts (5.538 s) +PASS src/api/request-size-limiter.test.ts (12.598 s) +PASS src/__tests__/integration.test.ts (11.907 s) + ● Console + + console.log + 2026-10-16T23:35:23.949Z warn: Event registry at capacity, evicting oldest events +{"maxEvents":2,"evicted":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/provider-registry.test.ts (21.115 s) + ● Console + + console.log + 2026-10-16T23:35:24.315Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock +Provider","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:24.728Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing +Mock","capabilities":[]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T23:35:24.758Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial +Mock","capabilities":["RICH_FORMATTING"]} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + +PASS src/services/notification-analytics-aggregator.test.ts (21.706 s) +PASS src/services/retry-statistics.test.ts (15.12 s) +A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due +to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure +that .unref() was called on them. + +Summary of all failing tests +FAIL src/__tests__/scheduler-concurrency.test.ts (50.234 s) + ● Scheduler worker concurrency › concurrency=3 processes up to 3 notifications in parallel, all exactly once + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   102 | }); +  103 | + > 104 | test('concurrency=3 processes up to 3 notifications in parallel, +all exactly once', async () => { +  | ^ +  105 | await seedDueNotifications(6); +  106 | await sleep(1300); +  107 | const { registry, state } = +makeTrackingRegistry(); + + at src/__tests__/scheduler-concurrency.test.ts:104:3 + at Object. (src/__tests__/scheduler-concurrency.test.ts:24:1) + +FAIL src/services/execution-metrics.test.ts (57.16 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   199 | }); +  200 | + > 201 | it('should return retry distribution breakdown', +async () => { +  | ^ +  202 | // Create notifications with different retry counts +  203 | // 0 retries: 2 successes +  204 | for (let i = 0; i < +2; i++) { + + at src/services/execution-metrics.test.ts:201:5 + at Object. (src/services/execution-metrics.test.ts:13:9) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + + ● Execution Metrics Deduplication › should handle empty database gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 2 + +   369 | const metrics = await +repository.getExecutionMetrics(); +  370 | + > 371 | +expect(metrics.totalNotifications).toBe(0); +  | ^ +  372 | expect(metrics.successfulFirstAttempt).toBe(0); +  373 | expect(metrics.successfulAfterRetry).toBe(0); +  374 | expect(metrics.permanentFailures).toBe(0); + + at Object. (src/services/execution-metrics.test.ts:371:40) + + ● Execution Metrics Deduplication › should only count COMPLETED and FAILED notifications, not PENDING + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   410 | +  411 | // Should only count the completed notification + > 412 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  413 | expect(metrics.successfulFirstAttempt).toBe(1); +  414 | }); +  415 | }); + + at Object. (src/services/execution-metrics.test.ts:412:40) + +FAIL src/api/queue-operational-metrics.test.ts (62.336 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +FAIL src/api/execution-metrics-api.test.ts (35.173 s) + ● Execution Metrics API Integration › GET /api/schedule/execution-metrics should return deduplicated metrics for +retried notifications + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › GET /api/schedule/execution-metrics should return deduplicated metrics for +retried notifications + + TypeError: Cannot read properties of undefined (reading 'close') + +   55 | // Close server +  56 | await new +Promise<void>((resolve, reject) => { + > 57 | server.close((err) => { +  | ^ +  58 | if (err) reject(err); +  59 | else resolve(); +  60 | }); + + at src/api/execution-metrics-api.test.ts:57:14 + at Object. (src/api/execution-metrics-api.test.ts:56:11) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + Server is not running. + + ● Execution Metrics API Integration › should return 503 when scheduler is not enabled + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › should return 503 when scheduler is not enabled + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › should return 503 when scheduler is not enabled + + Server is not running. + + ● Execution Metrics API Integration › should handle CORS preflight requests + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › should handle CORS preflight requests + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › should handle CORS preflight requests + + Server is not running. + + ● Execution Metrics API Integration › GET /api/schedule/stats should still work for notification-level statistics + + listen EADDRINUSE: address already in use :::38080 + +   46 | server = createEventsServer(options); +  47 | await new Promise<void>((resolve) +=> { + > 48 | server.listen(testPort, () => +resolve()); +  | ^ +  49 | }); +  50 | +  51 | serverUrl = `http://localhost:${testPort}`; + + at serverUrl (src/api/execution-metrics-api.test.ts:48:14) + at Object. (src/api/execution-metrics-api.test.ts:47:11) + + ● Execution Metrics API Integration › GET /api/schedule/stats should still work for notification-level statistics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   23 | const testPort = 38080; +  24 | + > 25 | beforeEach(async () => { +  | ^ +  26 | // Clean up any existing test database +  27 | try { +  28 | await fs.unlink(testDbPath); + + at src/api/execution-metrics-api.test.ts:25:13 + at Object. (src/api/execution-metrics-api.test.ts:16:9) + + ● Execution Metrics API Integration › GET /api/schedule/stats should still work for notification-level statistics + + Server is not running. + +FAIL src/database/query-performance.test.ts (42.008 s) + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   44 | }); +  45 | + > 46 | it('uses an indexed plan for pending claim queries', +async () => { +  | ^ +  47 | // Seed enough rows that a full scan would be meaningful +  48 | for (let i = 0; i < +200; i++) { +  49 | await db.run( + + at src/database/query-performance.test.ts:46:3 + at Object. (src/database/query-performance.test.ts:20:1) + +FAIL src/services/retry-deduplication.test.ts (111.599 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   47 |  * Ensures that a notification with max retries (3 attempts) only counts as 1 +failure +  48 |  */ + > 49 | it('should count max-retry exhausted notification as exactly 1 +failure', async () => { +  | ^ +  50 | const notificationId = await repository.create({ +  51 | payload: { test: 'max-retries' }, +  52 | notificationType: +NotificationType.DISCORD, + + at src/services/retry-deduplication.test.ts:49:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   175 | const metrics = await +repository.getExecutionMetrics(); +  176 | + > 177 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  178 | expect(metrics.successfulFirstAttempt).toBe(0); +  179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:177:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude PENDING and PROCESSING notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 13 + +   345 | +  346 | // Should only count the completed notification + > 347 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  348 | expect(metrics.successfulFirstAttempt).toBe(1); +  349 | }); +  350 | + + at Object. (src/services/retry-deduplication.test.ts:347:40) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 15 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 17 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 19 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 23 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   537 |  * Ensures getRetryDistribution() also uses deduplication +  538 |  */ + > 539 | it('should provide accurate retry distribution without +double-counting', async () => { +  | ^ +  540 | // Create notifications with specific retry patterns +  541 | const patterns = [ +  542 | { retries: 0, shouldSucceed: +true, count: 5 }, // 5 immediate successes + + at src/services/retry-deduplication.test.ts:539:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (120.242 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/services/concurrent-event-processing.test.ts (136.38 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 9) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   311 | } +  312 | + > 313 | it('two listener instances polling the same events notify each +event exactly once', async () => { +  | ^ +  314 | const instanceA = new +EventSubscriber(makeConfig(), new EventDeduplicationService(dbA)); +  315 | const instanceB = new +EventSubscriber(makeConfig(), new EventDeduplicationService(dbB)); +  316 | + + at src/services/concurrent-event-processing.test.ts:313:5 + at src/services/concurrent-event-processing.test.ts:286:3 + at Object. (src/services/concurrent-event-processing.test.ts:125:1) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/scheduled-notification-queue.test.ts (78.816 s) + ● Durable notification queue › refuses to renew a lease owned by another worker or already finished + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   36 | let repository: ScheduledNotificationRepository; +  37 | + > 38 | beforeEach(async () => { +  | ^ +  39 | await removeTestDatabase(); +  40 | +  41 | db = new Database(testDbPath); + + at src/services/scheduled-notification-queue.test.ts:38:3 + at Object. (src/services/scheduled-notification-queue.test.ts:34:1) + + ● Durable notification queue › does not dequeue a retry that is still inside its backoff window + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   36 | let repository: ScheduledNotificationRepository; +  37 | + > 38 | beforeEach(async () => { +  | ^ +  39 | await removeTestDatabase(); +  40 | +  41 | db = new Database(testDbPath); + + at src/services/scheduled-notification-queue.test.ts:38:3 + at Object. (src/services/scheduled-notification-queue.test.ts:34:1) + + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (139.223 s) + ● Notification delivery lifecycle (e2e) › happy path delivery › delivers a contract event to Discord and marks +fingerprint as sent + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   93 | // ========================================================================= +  94 | describe('happy path delivery', () => { + > 95 | it('delivers a contract event to Discord and marks fingerprint as +sent', async () => { +  | ^ +  96 | const service = new +DiscordNotificationService(discordConfig); +  97 | const result = await +service.sendEventNotification(makeEvent(), contractCfg, 'req-happy'); +  98 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:95:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › includes ledger number in the embed + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   114 | }); +  115 | + > 116 | it('includes ledger number in the embed', +async () => { +  | ^ +  117 | const service = new +DiscordNotificationService(discordConfig); +  118 | await service.sendEventNotification( +  119 | makeEvent({ txHash: 'tx-abc123', ledger: +42000 }), + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:116:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › happy path delivery › returns true and does not call fetch for duplicate +events (deduplication) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   131 | }); +  132 | + > 133 | it('returns true and does not call fetch for duplicate events +(deduplication)', async () => { +  | ^ +  134 | const service = new +DiscordNotificationService(discordConfig); +  135 | const event = makeEvent({ id: 'evt-dedup-1' +}); +  136 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:133:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:94:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders contract events with color 0xff00 + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event type color mapping › renders system events with color 0x99ff + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   239 | +  240 | for (const { type, expectedColor } of cases) { + > 241 | it(`renders ${type} events with color +0x${expectedColor.toString(16)}`, async () => { +  | ^ +  242 | const service = new +DiscordNotificationService(discordConfig); +  243 | await service.sendEventNotification(makeEvent({ type } +as any), contractCfg); +  244 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:241:7 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:234:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles multi-symbol topic arrays + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   253 | // ========================================================================= +  254 | describe('event topic rendering', () => { + > 255 | it('handles multi-symbol topic arrays', +async () => { +  | ^ +  256 | const service = new +DiscordNotificationService(discordConfig); +  257 | const event = makeEvent({ +  258 | topic: +[xdr.ScVal.scvSymbol('task'), +xdr.ScVal.scvSymbol('completed')], + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:255:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:254:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   284 | // ========================================================================= +  285 | describe('network errors', () => { + > 286 | it('returns false and logs error when fetch +throws', async () => { +  | ^ +  287 | fetchMock.mockRejectedValueOnce(new +Error('ECONNRESET')); +  288 | +  289 | const service = new +DiscordNotificationService(discordConfig); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:286:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   304 | }); +  305 | + > 306 | it('returns false on timeout (abort)', +async () => { +  | ^ +  307 | // Simulate a slow response that gets aborted +  308 | fetchMock.mockImplementationOnce( +  309 | () => + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:306:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:285:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › allows same event after dedup window expires + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   330 | // ========================================================================= +  331 | describe('deduplication', () => { + > 332 | it('allows same event after dedup window +expires', async () => { +  | ^ +  333 | const dedup = new NotificationDeduplicator({ +  334 | windowMs: 100, +  335 | maxSize: 100, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:332:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › deduplication › evicts oldest entry when cache is at capacity + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   355 | }); +  356 | + > 357 | it('evicts oldest entry when cache is at +capacity', async () => { +  | ^ +  358 | const dedup = new NotificationDeduplicator({ +maxSize: 2, windowMs: 60000 }); +  359 | const service = new +DiscordNotificationService(discordConfig, dedup); +  360 | + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:357:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:331:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + + ● Notification delivery lifecycle (e2e) › delivery report › DiscordNotificationService tracks metrics via +deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   409 | // ========================================================================= +  410 | describe('delivery report', () => { + > 411 | it('DiscordNotificationService tracks metrics via +deduplicator', async () => { +  | ^ +  412 | const service = new +DiscordNotificationService(discordConfig); +  413 | +  414 | await service.sendEventNotification(makeEvent({ id: +'evt-metrics-1' }), contractCfg); + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:411:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:410:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/services/discord-notification.test.ts (64.629 s) + ● DiscordNotificationService › sendEventNotification › should send event notification successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   58 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  59 | + > 60 | expect(result).toBe(true); +  | ^ +  61 | expect(mockFetch).toHaveBeenCalledTimes(1); +  62 | const [url, options] = +mockFetch.mock.calls[0]; +  63 | expect(url).toBe(mockConfig.webhookUrl); + + at Object. (src/services/discord-notification.test.ts:60:22) + + ● DiscordNotificationService › sendEventNotification › should handle request timeout + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Discord webhook request timed out", ObjectContaining {"timeoutMs": 100, "webhookId": "123"} + Received + 1: "Discord webhook request error", {"attempt": 0, "contractAddress": "CA123", "correlationId": +"baa1b6e4-2376-437b-b785-6fb071a66855", "durationMs": 311, "error": [DnsRebindingError: DNS resolution returned no +addresses for hostname: discord.com], "eventId": "event-123", "requestId": "baa1b6e4-2376-437b-b785-6fb071a66855", +"webhookId": "123"} + 2: "Exceeded max Discord retry attempts", {"contractAddress": "CA123", "correlationId": +"baa1b6e4-2376-437b-b785-6fb071a66855", "eventId": "event-123", "maxRetries": 0, "requestId": +"baa1b6e4-2376-437b-b785-6fb071a66855", "webhookId": "123"} + + Number of calls: 2 + +   111 | +  112 | expect(result).toBe(false); + > 113 | expect(mockLoggerModule.error).toHaveBeenCalledWith( +  | ^ +  114 | 'Discord webhook request timed out', +  115 | expect.objectContaining({ +  116 | webhookId: mockConfig.webhookId, + + at Object. (src/services/discord-notification.test.ts:113:38) + + ● DiscordNotificationService › duplicate detection › skips the webhook call for a duplicate event + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   133 | const secondResult = await +service.sendEventNotification(mockEvent, mockContractConfig); +  134 | + > 135 | +expect(mockFetch).toHaveBeenCalledTimes(1); +  | ^ +  136 | expect(secondResult).toBe(true); +  137 | expect(service.getDeduplicationMetrics()).toEqual( +  138 | expect.objectContaining({ skippedDuplicates: 1, +cacheSize: 1 }), + + at Object. (src/services/discord-notification.test.ts:135:25) + + ● DiscordNotificationService › duplicate detection › logs a duplicate detection event + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping duplicate notification", ObjectContaining {"contractAddress": "CA123", "eventId": +"event-dup-log"} + Received + 1: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"fd3ec92b-260f-4ba8-a4c8-e04a3ea73e7d", "eventId": "event-dup-log", "requestId": +"fd3ec92b-260f-4ba8-a4c8-e04a3ea73e7d", "webhookId": "123"} + 2: "Sending Discord notification", {"contractAddress": "CA123", "correlationId": +"4e30c2b1-4ef9-4f84-b4b2-4c6515b27851", "eventId": "event-dup-log", "requestId": +"4e30c2b1-4ef9-4f84-b4b2-4c6515b27851", "webhookId": "123"} + + Number of calls: 2 + +   151 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  152 | + > 153 | expect(mockLoggerModule.info).toHaveBeenCalledWith( +  | ^ +  154 | 'Skipping duplicate notification', +  155 | expect.objectContaining({ +  156 | eventId: 'event-dup-log', + + at Object. (src/services/discord-notification.test.ts:153:37) + + ● DiscordNotificationService › duplicate detection › allows the same notification request after the configured +window expires + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   173 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  174 | + > 175 | expect(result).toBe(true); +  | ^ +  176 | expect(mockFetch).toHaveBeenCalledTimes(2); +  177 | expect(service.getDeduplicationMetrics()).toEqual( +  178 | expect.objectContaining({ acceptedRequests: 2, +skippedDuplicates: 1 }), + + at Object. (src/services/discord-notification.test.ts:175:22) + + ● DiscordNotificationService › duplicate detection › allows the same event id on a different contract through + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 0 + +   191 | const result = await +service.sendEventNotification(mockEvent, contractB); +  192 | + > 193 | +expect(mockFetch).toHaveBeenCalledTimes(2); +  | ^ +  194 | expect(result).toBe(true); +  195 | }); +  196 | + + at Object. (src/services/discord-notification.test.ts:193:25) + + ● DiscordNotificationService › duplicate detection › does not mark an event as sent when the webhook call fails + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   213 | +  214 | expect(firstResult).toBe(false); + > 215 | expect(secondResult).toBe(true); +  | ^ +  216 | expect(mockFetch).toHaveBeenCalledTimes(2); +  217 | }); +  218 | + + at Object. (src/services/discord-notification.test.ts:215:28) + + ● DiscordNotificationService › duplicate detection › accepts an injected deduplicator + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   227 | await service.sendEventNotification(mockEvent, +mockContractConfig); +  228 | + > 229 | +expect(deduplicator.size()).toBe(1); +  | ^ +  230 | +expect(deduplicator.isDuplicate(generateFingerprint('event-injected', +'CA123'))).toBe(true); +  231 | }); +  232 | }); + + at Object. (src/services/discord-notification.test.ts:229:35) + + ● DiscordNotificationService › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   239 | const result = await +service.sendTestMessage(); +  240 | + > 241 | expect(result).toBe(true); +  | ^ +  242 | }); +  243 | +  244 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification.test.ts:241:22) + + ● DiscordNotificationService › formatEventMessage › should format event with string value correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   264 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  265 | + > 266 | expect(result).toBe(true); +  | ^ +  267 | const [, options] = +mockFetch.mock.calls[0]; +  268 | const body = +JSON.parse(options.body); +  269 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:266:22) + + ● DiscordNotificationService › formatEventMessage › should truncate long string values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   283 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  284 | + > 285 | expect(result).toBe(true); +  | ^ +  286 | const [, options] = +mockFetch.mock.calls[0]; +  287 | const body = +JSON.parse(options.body); +  288 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:285:22) + + ● DiscordNotificationService › formatEventMessage › should handle symbol type values + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   302 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  303 | + > 304 | expect(result).toBe(true); +  | ^ +  305 | const [, options] = +mockFetch.mock.calls[0]; +  306 | const body = +JSON.parse(options.body); +  307 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:304:22) + + ● DiscordNotificationService › formatEventMessage › should preserve short messages without truncation + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   320 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  321 | + > 322 | expect(result).toBe(true); +  | ^ +  323 | const [, options] = +mockFetch.mock.calls[0]; +  324 | const body = +JSON.parse(options.body); +  325 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:322:22) + + ● DiscordNotificationService › formatEventMessage › should truncate oversized embed titles + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   340 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  341 | + > 342 | expect(result).toBe(true); +  | ^ +  343 | const [, options] = +mockFetch.mock.calls[0]; +  344 | const body = +JSON.parse(options.body); +  345 | expect(body.embeds[0].title.length).toBeL +essThanOrEqual(256); + + at Object. (src/services/discord-notification.test.ts:342:22) + + ● DiscordNotificationService › formatEventMessage › should enforce total embed size limit + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   361 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  362 | + > 363 | expect(result).toBe(true); +  | ^ +  364 | const [, options] = +mockFetch.mock.calls[0]; +  365 | const body = +JSON.parse(options.body); +  366 | const embed = body.embeds[0]; + + at Object. (src/services/discord-notification.test.ts:363:22) + + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   380 | const result = await +service.sendEventNotification(mockEvent, mockContractConfig); +  381 | + > 382 | expect(result).toBe(true); +  | ^ +  383 | const [, options] = +mockFetch.mock.calls[0]; +  384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + + at Object. (src/services/discord-notification.test.ts:382:22) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone +embedded in an scvString payload + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   459 | +  460 | describe('integration: mention in event string value reaches embed +sanitized', () => { + > 461 | it('strips @everyone embedded in an scvString +payload', async () => { +  | ^ +  462 | mockFetch.mockResolvedValueOnce({ ok: true }); +  463 | +  464 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:461:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + + ● sanitizeForDiscord › integration: mention in event string value reaches embed sanitized › strips @everyone in +event topic symbol used as embed title + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   487 | }); +  488 | + > 489 | it('strips @everyone in event topic symbol used as embed +title', async () => { +  | ^ +  490 | mockFetch.mockResolvedValueOnce({ ok: true }); +  491 | +  492 | const service = new +DiscordNotificationService({ + + at src/services/discord-notification.test.ts:489:7 + at src/services/discord-notification.test.ts:460:11 + at Object. (src/services/discord-notification.test.ts:391:9) + +FAIL src/services/notification-failure-scenarios.test.ts (37.002 s) + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › tracks timeoutCount in DiscordNotificationService on +AbortError + + expect(received).toBeGreaterThanOrEqual(expected) + + Expected: >= 1 + Received: 0 + +   71 | const success = await +service.sendTestMessage(); +  72 | expect(success).toBe(false); + > 73 | +expect(service.getMetrics().timeoutCount).toBeGreaterThanOrEqual(1); +  | ^ +  74 | }); +  75 | }); +  76 | + + at Object. (src/services/notification-failure-scenarios.test.ts:73:49) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/database/event-notification-indexes.test.ts (5.628 s) + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/tests/template-system.test.ts (8.027 s) + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/__tests__/retry-scheduler.integration.test.ts + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/database/database-transaction.test.ts (5.894 s) + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +4456.171887346246, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:30:29.008Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/tests/template-api-integration.test.ts (25.466 s) + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/tests/payload-integrity.test.ts (54.333 s) + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/api/template-routes.test.ts (10.108 s) + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/api/events-server.test.ts (28.112 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/discord-notification-refactored.test.ts (156.845 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendTestMessage › should send test message successfully + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   243 | const result = await +service.sendTestMessage(); +  244 | + > 245 | expect(result).toBe(true); +  | ^ +  246 | }); +  247 | +  248 | it('should handle test message failure', async () => +{ + + at Object. (src/services/discord-notification-refactored.test.ts:245:22) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/utils/log-redaction.test.ts (15.171 s) + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/api/auth-protected-endpoints.test.ts (15.987 s) + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/services/archive.test.ts (19.752 s) + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (168.472 s) + ● Multi-channel notification delivery (e2e) › fan-out delivery › delivers a single event to every configured channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   169 | +  170 | describe('fan-out delivery', () => { + > 171 | it('delivers a single event to every configured +channel', async () => { +  | ^ +  172 | const results = await +deliverToAllChannels(createMockEvent(), 'req-fanout'); +  173 | +  174 | expect(results).toEqual({ alerts: true, +ops: true, audit: true }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:171:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › fan-out delivery › routes each channel its own payload at its own +webhook URL + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   178 | }); +  179 | + > 180 | it('routes each channel its own payload at its own webhook +URL', async () => { +  | ^ +  181 | await deliverToAllChannels(createMockEvent({ id: +'event-routing' })); +  182 | +  183 | for (const channel of channels) { + + at src/__tests__/multi-channel-delivery.e2e.test.ts:180:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:170:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › builds a Discord embed payload carrying +the event details + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   194 | +  195 | describe('channel-specific payloads', () => { + > 196 | it('builds a Discord embed payload carrying the event +details', async () => { +  | ^ +  197 | await deliverToAllChannels( +  198 | createMockEvent({ type: 'contract', value: +xdr.ScVal.scvString('payload-check') }) +  199 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:196:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › channel-specific payloads › reflects the event type in the payload +color across channels + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   210 | }); +  211 | + > 212 | it('reflects the event type in the payload color across +channels', async () => { +  | ^ +  213 | await deliverToAllChannels( +  214 | createMockEvent({ id: 'event-system', type: +'system' }) +  215 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:212:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:195:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +FAIL src/services/notification-retry-queue-refactored.test.ts (12.807 s) + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 4134.731315203808, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T23:30:53.688Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/services/webhook-verifier.test.ts (20.092 s) + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +FAIL src/services/notification-expiration.test.ts (14.937 s) + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/config.test.ts (29.731 s) + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/services/notification-deduplicator.test.ts (19.785 s) + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/cleanup-service.test.ts (12.743 s) + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/__tests__/config-startup-validation.test.ts (11.796 s) + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/services/webhook-retry-helper.test.ts (35.703 s) + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/api/request-duration-logger.test.ts (26.362 s) + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +FAIL src/services/event-subscriber-reorg.test.ts (21.827 s) + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "e11afc94"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "eac40715-9d47-4dc0-a6dd-783e2d43ac8f", +"eventId": "event-1", "requestId": "eac40715-9d47-4dc0-a6dd-783e2d43ac8f"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/services/providers/provider-capabilities.test.ts (25.996 s) + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/notification-claim-lease.test.ts (11.844 s) + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/services/webhook-delivery-service.test.ts (48.326 s) + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 1, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/services/retry-policy-integration.test.ts (45.45 s) + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/services/event-subscriber.test.ts (27.39 s) + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792107082912} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792193485593, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792107082912} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792193485593, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "ca09b146"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/tests/api-versioning.test.ts (20.304 s) + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/services/notification-api-webhook.test.ts (13.209 s) + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/services/event-subscriber-backfill.test.ts (28.716 s) + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts (19.326 s) + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/api/archive-api.test.ts (13.909 s) + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/services/retry-scheduler-webhook.test.ts (30.429 s) + ● RetryScheduler — webhook retry queue › webhook timeout configuration › applies the configured webhookTimeoutMs to +the outbound request + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   494 | await scheduler.runOnce(); +  495 | + > 496 | expect(aborted).toBe(true); +  | ^ +  497 | expect(repo.markAsFailedOrRetry).toHaveBeenCalledWith( +  498 | 10, +  499 | expect.objectContaining({ message: +expect.stringContaining('timed out after 25ms') }), + + at Object. (src/services/retry-scheduler-webhook.test.ts:496:23) + +FAIL src/__tests__/notification-flow-e2e.test.ts (79.834 s) + ● 5. Backpressure and idempotency integration › activates backpressure when queue exceeds saturation threshold + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › calculates a positive processing delay under backpressure + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › deactivates backpressure once the queue drops below recovery +threshold + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › records a backpressure activation event and surfaces it in statistics + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › computes average duration across activation/deactivation pairs + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + ● 5. Backpressure and idempotency integration › cleans up expired idempotency keys after 24 hours + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   816 | let backpressureMonitor: BackpressureMonitor; +  817 | + > 818 | beforeAll(async () => { +  | ^ +  819 | db = await setupDb(); +  820 | repository = new +ScheduledNotificationRepository(db); +  821 | idempotencyRepo = new +IdempotencyKeyRepository(db); + + at src/__tests__/notification-flow-e2e.test.ts:818:3 + at Object. (src/__tests__/notification-flow-e2e.test.ts:809:1) + + + ● Test suite failed to run + + TypeError: Cannot read properties of undefined (reading 'close') + +   832 | +  833 | afterAll(async () => { + > 834 | await db.close(); +  | ^ +  835 | removeDbFile(); +  836 | }); +  837 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:834:14) + +FAIL src/tests/notification-scheduler.test.ts (86.508 s) + ● NotificationScheduler › ScheduledNotificationRepository › persists successful provider delivery with sanitized +response metadata + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   56 | +  57 | describe('ScheduledNotificationRepository', () => { + > 58 | test('persists successful provider delivery with sanitized +response metadata', async () => { +  | ^ +  59 | const id = await repository.create({ +  60 | payload: { message: 'Receipt test' }, +  61 | notificationType: +NotificationType.WEBHOOK, + + at src/tests/notification-scheduler.test.ts:58:5 + at src/tests/notification-scheduler.test.ts:57:3 + at Object. (src/tests/notification-scheduler.test.ts:16:1) + + ● NotificationScheduler › ScheduledNotificationRepository › persists a failed receipt with timeout details when +provider transport throws + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | }); +  107 | + > 108 | test('persists a failed receipt with timeout details when +provider transport throws', async () => { +  | ^ +  109 | const id = await repository.create({ +  110 | payload: { message: 'Timeout receipt test' }, +  111 | notificationType: +NotificationType.WEBHOOK, + + at src/tests/notification-scheduler.test.ts:108:5 + at src/tests/notification-scheduler.test.ts:57:3 + at Object. (src/tests/notification-scheduler.test.ts:16:1) + + ● NotificationScheduler › ScheduledNotificationRepository › does not deliver a notification that expires between +retries + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   196 | }); +  197 | + > 198 | test('does not deliver a notification that expires between +retries', async () => { +  | ^ +  199 | const id = await repository.create({ +  200 | payload: { event: {}, contractConfig: +{}, message: 'Retry expiration' }, +  201 | notificationType: +NotificationType.DISCORD, + + at src/tests/notification-scheduler.test.ts:198:5 + at src/tests/notification-scheduler.test.ts:57:3 + at Object. (src/tests/notification-scheduler.test.ts:16:1) + +FAIL src/api/queue-visibility.test.ts (98.727 s) + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns an empty list when no +notifications are pending + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   166 | +  167 | describe('GET /api/schedule/queue — pending jobs', () => { + > 168 | it('returns an empty list when no notifications are +pending', async () => { +  | ^ +  169 | const { status, body } = await +makeRequest(server, 'GET', '/api/schedule/queue'); +  170 | +  171 | expect(status).toBe(200); + + at src/api/queue-visibility.test.ts:168:7 + at src/api/queue-visibility.test.ts:167:11 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns an empty list when no +notifications are pending + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   96 | }); +  97 | + > 98 | afterEach(async () => { +  | ^ +  99 | await new +Promise<void>((resolve, reject) => +  100 | server.close((err) => (err ? reject(err) : +resolve())), +  101 | ); + + at src/api/queue-visibility.test.ts:98:12 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › does not return completed or failed +notifications + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › does not return completed or failed +notifications + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › respects the ?limit= query parameter + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › respects the ?limit= query parameter + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › respects the ?limit= query parameter + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns 503 when the scheduler is not +enabled + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns 503 when the scheduler is not +enabled + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › returns 503 when the scheduler is not +enabled + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › is accessible via the /api/v1/ +version alias + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › is accessible via the /api/v1/ +version alias + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/schedule/queue — pending jobs › is accessible via the /api/v1/ +version alias + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 503 with an +error message when the health monitor is not configured + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › returns 200 with a +health report when the monitor is configured and healthy + + Server is not running. + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + listen EADDRINUSE: address already in use :::38090 + +   93 | +  94 | server = createEventsServer(options); + > 95 | await new +Promise<void>((resolve) => +server.listen(TEST_PORT, resolve)); +  | ^ +  96 | }); +  97 | +  98 | afterEach(async () => { + + at src/api/queue-visibility.test.ts:95:49 + at Object. (src/api/queue-visibility.test.ts:95:11) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   73 | let server: http.Server; +  74 | + > 75 | beforeEach(async () => { +  | ^ +  76 | // Clean up any leftover test database from a previous run. +  77 | try { +  78 | await fs.unlink(TEST_DB_PATH); + + at src/api/queue-visibility.test.ts:75:13 + at Object. (src/api/queue-visibility.test.ts:69:9) + + ● Queue Visibility API (issue #490) › GET /api/notifications/health — queue health signal › reports +deadLetterQueueDepth in the queue sub-section + + Server is not running. + +FAIL src/__tests__/stress.test.ts (341.047 s) + ● Stress Test Suite: Notification Processing › 1. High-Volume Concurrent Processing › should handle 5,000 events +with sustained throughput + + expect(received).toBeLessThan(expected) + + Expected: < 200 + Received: 303 + +   252 | expect(processedIds.length).toBe(eventCount); +  253 | expect(report.throughput).toBeGreaterThan(100); + > 254 | +expect(report.latency.p99).toBeLessThan(200); +  | ^ +  255 | }, 120000); +  256 | }); +  257 | + + at Object. (src/__tests__/stress.test.ts:254:34) + + ● Stress Test Suite: Notification Processing › 2. Sustained Load Over Time › should maintain stability processing +10,000 events + + expect(received).toBeLessThan(expected) + + Expected: < 50 + Received: 78.78 + +   319 | expect(processedIds.length).toBe(eventCount); +  320 | expect(report.throughput).toBeGreaterThan(100); + > 321 | +expect(report.latency.avg).toBeLessThan(50); +  | ^ +  322 | +  323 | // Check memory doesn't grow excessively (less than 200MB increase) +  324 | const memoryIncreaseMB = (memoryAfter.heapUsed - +memoryBefore.heapUsed) / (1024 * 1024); + + at Object. (src/__tests__/stress.test.ts:321:34) + + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + + +Test Suites: 51 failed, 1 skipped, 100 passed, 151 of 152 total +Tests: 312 failed, 4 skipped, 2236 passed, 2552 total +Snapshots: 0 total +Time: 464.955 s, estimated 3524 s +Ran all test suites. diff --git a/listener/_nsr_648.txt b/listener/_nsr_648.txt new file mode 100644 index 00000000..4e938255 --- /dev/null +++ b/listener/_nsr_648.txt @@ -0,0 +1,49 @@ +diff --git a/listener/src/tests/notification-scheduler-refactored.test.ts b/listener/src/tests/notification-scheduler-refactored.test.ts +index bce2ba2..9a44d50 100644 +--- a/listener/src/tests/notification-scheduler-refactored.test.ts ++++ b/listener/src/tests/notification-scheduler-refactored.test.ts +@@ -306,34 +306,27 @@ describe('NotificationScheduler (Refactored)', () => { + .build() + ); + +- // 2. Create a notification in the past (overdue, pending) +- await repository.create( ++ // 2. Create a notification in the past that is currently PROCESSING but lock is expired ++ const staleId = await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); ++ await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); ++ const pastLock = NotificationFixtureBuilder.dates.past(1000); ++ await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ ++ pastLock.toISOString(), ++ staleId, ++ ]); + +- // 3. Create a notification in the past that is currently PROCESSING but lock is expired +- const staleId = await repository.create( ++ // 3. Create a notification in the past (overdue, pending) - created AFTER locking to remain in PENDING status ++ await repository.create( + NotificationFixtureBuilder + .aScheduledNotificationInput() + .forImmediateExecution() + .build() + ); +- const pastLock = NotificationFixtureBuilder.dates.past(1000); +- await db.run( +- `UPDATE scheduled_notifications +- SET status = ?, processor_id = ?, lock_expires_at = ?, processing_started_at = ? +- WHERE id = ?`, +- [ +- NotificationStatus.PROCESSING, +- 'processor-1', +- pastLock.toISOString(), +- pastLock.toISOString(), +- staleId, +- ] +- ); + + // Get stats BEFORE recovery + const stats = await repository.getStats(); diff --git a/listener/_run2.txt b/listener/_run2.txt new file mode 100644 index 00000000..2755be89 --- /dev/null +++ b/listener/_run2.txt @@ -0,0 +1,13003 @@ +node : ts-jest[ts-compiler] (WARN) src/services/webhook-verifier.test.ts:601:20 - +error TS2339: Property 'authenticated' does not exist on type 'Promise'. +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=2 --wor ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (ts-jest[ts-comp...ationOutcome>'.:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + +601 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:601:20 + 601 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:602:20 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +602 expect(outcome.statusCode).toBe(401); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:602:20 + 602 expect(outcome.statusCode).toBe(401); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:603:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +603 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:603:20 + 603 expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:613:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +613 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:613:20 + 613 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:628:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +628 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:628:20 + 628 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:639:21 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +639 expect(replay.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:639:21 + 639 expect(replay.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:640:21 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +640 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:640:21 + 640 expect(replay.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:657:18 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +657 expect(first.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:657:18 + 657 expect(first.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:668:19 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +668 expect(second.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:668:19 + 668 expect(second.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:669:19 - error TS2339: Property +'statusCode' does not exist on type 'Promise'. + +669 expect(second.statusCode).toBe(409); +   ~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:669:19 + 669 expect(second.statusCode).toBe(409); +    ~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:670:19 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +670 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:670:19 + 670 expect(second.errorCode).toBe('AUTH_REPLAY_DETECTED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:695:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +695 expect(a.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:695:14 + 695 expect(a.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:696:14 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +696 expect(b.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:696:14 + 696 expect(b.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:709:16 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +709 expect(bad.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:709:16 + 709 expect(bad.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:718:17 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +718 expect(good.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:718:17 + 718 expect(good.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:729:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +729 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:729:20 + 729 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:730:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +730 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:730:20 + 730 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:740:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +740 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:740:20 + 740 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:741:20 - error TS2339: Property +'errorCode' does not exist on type 'Promise'. + +741 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +   ~~~~~~~~~ + + src/services/webhook-verifier.test.ts:741:20 + 741 expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +    ~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:756:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +756 expect(outcome.authenticated).toBe(true); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:756:20 + 756 expect(outcome.authenticated).toBe(true); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +src/services/webhook-verifier.test.ts:770:20 - error TS2339: Property +'authenticated' does not exist on type 'Promise'. + +770 expect(outcome.authenticated).toBe(false); +   ~~~~~~~~~~~~~ + + src/services/webhook-verifier.test.ts:770:20 + 770 expect(outcome.authenticated).toBe(false); +    ~~~~~~~~~~~~~ + Did you forget to use 'await'? +FAIL src/services/webhook-verifier.test.ts (18.67 s) + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +ts-jest[ts-compiler] (WARN) src/__tests__/blockchain-to-notification-e2e.test.ts:153:34 - +error TS2345: Argument of type '{ events: never[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: never[]; cursor: string; }' is missing the following properties from type 'GetEventsResponse': +latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +153 expect(validateRpcResponse(response).valid).toBe(true); +   ~~~~~~~~ +src/__tests__/blockchain-to-notification-e2e.test.ts:867:42 - error TS2345: +Argument of type '{ events: StellarSDK.rpc.Api.EventResponse[]; cursor: string; }' is not assignable to parameter +of type 'GetEventsResponse'. + Type '{ events: EventResponse[]; cursor: string; }' is missing the following properties from type +'GetEventsResponse': latestLedger, oldestLedger, latestLedgerCloseTime, oldestLedgerCloseTime + +867 const rpcCheck = validateRpcResponse(rpcResponse); +   ~~~~~~~~~~~ +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:36.589Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:36.652Z info: Worker manager force shutdown completed +2026-06-24T12:05:01.400Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:05:01.400Z info: No active jobs - proceeding with shutdown +2026-06-24T12:05:01.400Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:36.714Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:36.768Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:36.869Z info: Worker manager force shutdown completed +2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} +2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown +2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:36.937Z info: Worker manager force shutdown completed +2026-10-16T12:37:38.017Z info: Worker manager force shutdown completed +2026-10-16T12:37:39.075Z info: Worker manager force shutdown completed +2026-10-16T12:37:40.225Z info: Worker manager force shutdown completed +2026-10-16T12:37:41.414Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:41.424Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:41.427Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:41.429Z info: Worker manager force shutdown completed +2026-10-16T12:37:41.536Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:41.536Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:41.537Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:41.537Z info: Worker manager force shutdown completed +2026-10-16T12:37:41.870Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:41.870Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:41.871Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:41.871Z info: Worker manager force shutdown completed +2026-10-16T12:37:42.420Z info: Worker manager force shutdown completed +2026-10-16T12:37:42.815Z info: Worker manager force shutdown completed +2026-10-16T12:37:42.962Z info: Worker manager force shutdown completed +2026-10-16T12:37:43.537Z info: Worker manager force shutdown completed +2026-10-16T12:37:45.800Z info: Worker manager force shutdown completed +2026-10-16T12:37:45.966Z info: Worker manager force shutdown completed +FAIL src/__tests__/notification-flow-e2e.test.ts (36.358 s) + ● 1. Notification creation › rejects a duplicate idempotency key used with a different payload + + expect(received).rejects.toThrow(expected) + + Expected substring: "Idempotency key reused with different request body" + Received message: "Idempotency key reused with a different request body" + +   131 | if (!isValidRequest) { +  132 | logger.warn('Idempotency key reused with different request body +(replay suspected)', logMeta); + > 133 | throw new +IdempotencyKeyReuseError(); +  | ^ +  134 | } +  135 | +  136 | logger.info('Replay intercepted: returning cached idempotent +response', { + + at IdempotencyKeyService.processWithIdempotency (src/services/idempotency-key-service.ts:133:15) + at NotificationAPI.scheduleNotification (src/services/notification-api.ts:168:9) + at Object. (src/__tests__/notification-flow-e2e.test.ts:242:5) + at Object.toThrow (node_modules/expect/build/index.js:2155:20) + at Object. (src/__tests__/notification-flow-e2e.test.ts:248:15) + + ● 2. Delivery workflows › processes a due notification and transitions it to COMPLETED + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   332 | +  333 | const row = await repository.getById(id); + > 334 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  335 | expect(sendMock).toHaveBeenCalledTimes(1); +  336 | }); +  337 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:334:25) + + ● 2. Delivery workflows › delivers once executeAt elapses mid-run + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   366 | await jest.advanceTimersByTimeAsync(400); +  367 | + > 368 | expect((await repository.getById(id))!. +status).toBe(NotificationStatus.COMPLETED); +  | ^ +  369 | expect(sendMock).toHaveBeenCalledTimes(1); +  370 | }); +  371 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:368:52) + + ● 2. Delivery workflows › writes a SUCCESS execution log entry after delivery + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 0 + Received array: [] + +   385 | [id] +  386 | ); + > 387 | expect(logs).toHaveLength(1); +  | ^ +  388 | expect(logs[0].status).toBe('SUCCESS'); +  389 | expect(logs[0].execution_attempt).toBe(1); +  390 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:387:18) + + ● 2. Delivery workflows › processes a batch of multiple due notifications + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   407 | +  408 | for (const id of ids) { + > 409 | expect((await repository.getById(id))![33 +m.status).toBe(NotificationStatus.COMPLETED); +  | ^ +  410 | } +  411 | expect(sendMock).toHaveBeenCalledTimes(3); +  412 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:409:54) + + ● 2. Delivery workflows › forwards the event and contract config to the Discord service + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: ObjectContaining {"id": "deliver-evt-1"}, ObjectContaining {"address": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"}, Any + + Number of calls: 0 + +   427 | await jest.advanceTimersByTimeAsync(250); +  428 | + > 429 | expect(sendMock).toHaveBeenCalledWith( +  | ^ +  430 | expect.objectContaining({ id: 'deliver-evt-1' }), +  431 | expect.objectContaining({ address: contractConfig.address +}), +  432 | expect.any(String) + + at Object. (src/__tests__/notification-flow-e2e.test.ts:429:22) + + ● 3. Retry scenarios › prevents concurrent duplicate processing via distributed lock + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   576 | await p1; +  577 | + > 578 | expect(sendMock).toHaveBeenCalledTimes(1); +  | ^ +  579 | const row = await db.get<{ +status: string }>( +  580 | 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] +  581 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:578:22) + + ● 3. Retry scenarios › retries a failed delivery on the next scheduler poll cycle + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   609 | +  610 | const row = await repository.getById(id); + > 611 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  612 | expect(sendMock).toHaveBeenCalledTimes(2); +  613 | +  614 | await scheduler.stop(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:611:25) + + ● 4. Failure handling › marks a notification FAILED after exhausting all retries + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   653 | 'SELECT status, retry_count FROM scheduled_notifications WHERE id = +?', [id] +  654 | ); + > 655 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  656 | }); +  657 | +  658 | it('logs a permanent-failure error after max retries are exhausted', +async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:655:25) + + ● 4. Failure handling › logs a permanent-failure error after max retries are exhausted + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"id": 2, "totalAttempts": 3} + Received: "Notification permanently failed after max retries", {"failureType": "unknown", "id": 2, "requestId": +"e2e-req-id", "totalAttempts": 4} + + Number of calls: 1 + +   666 | await retryScheduler.runOnce(); +  667 | + > 668 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  669 | 'Notification permanently failed after max retries', +  670 | expect.objectContaining({ id, totalAttempts: 3 }) +  671 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:668:26) + + ● 4. Failure handling › transitions scheduler-dispatched notification to FAILED when retries run out + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PENDING" + +   727 | +  728 | const row = await repository.getById(id); + > 729 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  730 | +  731 | await scheduler.stop(); +  732 | jest.useRealTimers(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:729:25) + + ● 5. Backpressure and idempotency integration › activates backpressure when queue exceeds saturation threshold + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › calculates a positive processing delay under backpressure + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › deactivates backpressure once the queue drops below recovery +threshold + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › records a backpressure activation event and surfaces it in statistics + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › computes average duration across activation/deactivation pairs + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › cleans up expired idempotency keys after 24 hours + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + + ● Test suite failed to run + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   632 | afterAll(async () => { +  633 | await db.close(); + > 634 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  635 | }); +  636 | +  637 | beforeEach(async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:634:41) + + ● Test suite failed to run + + TypeError: Cannot read properties of undefined (reading 'close') + +   814 | +  815 | afterAll(async () => { + > 816 | await db.close(); +  | ^ +  817 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  818 | }); +  819 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:816:14) + +2026-10-16T12:37:47.082Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:47.083Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:47.083Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:47.083Z info: Worker manager force shutdown completed +2026-10-16T12:37:48.069Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:48.070Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:48.070Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:48.071Z info: Worker manager force shutdown completed +2026-10-16T12:37:48.247Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:48.248Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:48.248Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:48.249Z info: Worker manager force shutdown completed +2026-10-16T12:37:56.373Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:56.377Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:56.377Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:56.378Z info: Worker manager force shutdown completed +2026-10-16T12:37:57.952Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:37:57.952Z info: No active jobs - proceeding with shutdown +2026-10-16T12:37:57.953Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:37:57.954Z info: Worker manager force shutdown completed +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (29.081 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/api/events-server.test.ts (11.579 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +ts-jest[ts-compiler] (WARN) src/services/event-subscriber.test.ts:739:9 - +error TS2353: Object literal may only specify known properties, and 'receivedAt' does not exist in +type 'Partial'. + +739 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:772:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +772 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:799:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +799 receivedAt: expiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:835:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +835 receivedAt: veryOldTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:861:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +861 receivedAt: NOW - (365 * 24 * 60 * 60 * 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:884:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +884 receivedAt: NOW - (DEFAULT_EXPIRATION_MS + 1000), +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:888:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +888 receivedAt: NOW, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:924:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +924 receivedAt: fastEventExpiredTime, +   ~~~~~~~~~~ +src/services/event-subscriber.test.ts:928:9 - error TS2353: Object +literal may only specify known properties, and 'receivedAt' does not exist in type 'Partial'. + +928 receivedAt: NOW - 60 * 60 * 1000, // 1 hour ago (within default 24h) +   ~~~~~~~~~~ +PASS src/services/historical-backfill-service.test.ts +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792067878768} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792154279951, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792067878768} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792154279951, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "77ed391b"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +ts-jest[ts-compiler] (WARN) src/services/retry-policy-integration.test.ts:631:7 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +631 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:658:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +658 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:680:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +680 baseDelayMs: 10, +   ~~~~~~~~~~~ +src/services/retry-policy-integration.test.ts:702:7 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +702 baseDelayMs: 10, +   ~~~~~~~~~~~ +2026-10-16T12:38:01.521Z info: Connected to SQLite database {"path":"./data/test-notifications.db"} +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +2026-10-16T12:38:02.623Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:02.624Z info: Database migrations completed +2026-10-16T12:38:02.627Z info: Database initialized successfully {"path":"./data/test-notifications.db"} +2026-10-16T12:38:02.663Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T12:38:02.679Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} +2026-10-16T12:38:02.680Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-success-test"} +2026-10-16T12:38:02.682Z info: Processing scheduled notification {"requestId":"test-request-id","id":1,"type":"webhook","executeAt":{},"attempt":1} +2026-10-16T12:38:02.682Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":1,"executeAt":{},"now":{},"missedByMs":1035} +2026-10-16T12:38:02.711Z info: Notification marked as completed {"requestId":"test-request-id","id":1} +2026-10-16T12:38:02.722Z info: Notification delivered successfully {"requestId":"test-request-id","id":1,"type":"webhook","durationMs":14} +2026-10-16T12:38:02.723Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"receipt-success-test","count":1,"durationMs":59} +2026-10-16T12:38:02.731Z info: Worker manager force shutdown completed +2026-10-16T12:38:02.782Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} +2026-10-16T12:38:02.798Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} +2026-10-16T12:38:02.798Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"receipt-failure-test"} +2026-10-16T12:38:02.799Z info: Processing scheduled notification {"requestId":"test-request-id","id":2,"type":"webhook","executeAt":{},"attempt":1} +2026-10-16T12:38:02.800Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":2,"executeAt":{},"now":{},"missedByMs":1030} +2026-10-16T12:38:02.809Z error: Failed to process notification {"requestId":"test-request-id","id":2,"error":{"message":"secret token=should-not-persist","name":"AbortError","stack":"AbortError: secret token=should-not-persist\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:115:42)"},"attempt":1,"durationMs":1} +2026-10-16T12:38:02.822Z error: Job monitor: job failed {"jobId":"notification-2","jobName":"scheduled-notification","error":"secret token=should-not-persist","durationMs":23} +2026-10-16T12:38:02.835Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:38:07.823Z"} +2026-10-16T12:38:02.846Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"receipt-failure-test","count":1,"durationMs":62} +2026-10-16T12:38:02.848Z info: Worker manager force shutdown completed +2026-10-16T12:38:02.905Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:38:02.949Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:38:02.965Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"expired-dispatch-test"} +2026-10-16T12:38:02.997Z info: Expired notification skipped before delivery {"requestId":"test-request-id","id":4} +2026-10-16T12:38:03.049Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.068Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} +2026-10-16T12:38:03.069Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"retry-expiration-initial-test"} +2026-10-16T12:38:03.071Z info: Processing scheduled notification {"requestId":"test-request-id","id":5,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:38:03.071Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":5,"executeAt":{},"now":{},"missedByMs":60035} +2026-10-16T12:38:03.072Z error: Failed to process notification {"requestId":"test-request-id","id":5,"error":{"message":"temporary outage","name":"Error","stack":"Error: temporary outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:208:64)"},"attempt":1,"durationMs":1} +2026-10-16T12:38:03.072Z error: Job monitor: job failed {"jobId":"notification-5","jobName":"scheduled-notification","error":"temporary outage","durationMs":2} +2026-10-16T12:38:03.088Z info: Notification marked for retry or failed {"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:38:03.173Z"} +2026-10-16T12:38:03.102Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"retry-expiration-initial-test","count":1,"durationMs":51} +2026-10-16T12:38:03.180Z info: Fetched due retries {"requestId":"test-request-id","count":1,"processorId":"retry-expiration-test"} +2026-10-16T12:38:03.182Z info: RetryScheduler processing batch {"requestId":"test-request-id","processorId":"retry-expiration-test","count":1} +2026-10-16T12:38:03.211Z info: Expired retry skipped before delivery {"requestId":"test-request-id","id":5} +2026-10-16T12:38:03.214Z info: Worker manager force shutdown completed +2026-10-16T12:38:03.280Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.296Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.312Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} +2026-10-16T12:38:03.313Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":2,"processorId":"valid-expiration-test"} +2026-10-16T12:38:03.316Z info: Processing scheduled notification {"requestId":"test-request-id","id":6,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:38:03.316Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":6,"executeAt":{},"now":{},"missedByMs":1048} +2026-10-16T12:38:03.331Z info: Notification marked as completed {"requestId":"test-request-id","id":6} +2026-10-16T12:38:03.345Z info: Notification delivered successfully {"requestId":"test-request-id","id":6,"type":"discord","durationMs":2} +2026-10-16T12:38:03.348Z info: Processing scheduled notification {"requestId":"test-request-id","id":7,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:38:03.349Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":7,"executeAt":{},"now":{},"missedByMs":1067} +2026-10-16T12:38:03.362Z info: Notification marked as completed {"requestId":"test-request-id","id":7} +2026-10-16T12:38:03.374Z info: Notification delivered successfully {"requestId":"test-request-id","id":7,"type":"discord","durationMs":1} +2026-10-16T12:38:03.378Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"valid-expiration-test","count":2,"durationMs":80} +2026-10-16T12:38:03.380Z info: Worker manager force shutdown completed +2026-10-16T12:38:03.431Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.446Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.463Z info: Fetched and locked pending notifications {"count":2,"processorId":"test-processor-1"} +2026-10-16T12:38:03.506Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.540Z info: Fetched and locked pending notifications {"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} +2026-10-16T12:38:03.541Z info: Processing batch of scheduled notifications {"requestId":"test-request-id","count":1,"processorId":"dispatcher-test"} +2026-10-16T12:38:03.543Z info: Processing scheduled notification {"requestId":"test-request-id","id":10,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:38:03.544Z warn: Missed scheduled notification detected; dispatching catch-up delivery {"requestId":"test-request-id","id":10,"executeAt":{},"now":{},"missedByMs":60050} +2026-10-16T12:38:03.545Z error: Failed to process notification {"requestId":"test-request-id","id":10,"error":{"message":"temporary discord outage","name":"Error","stack":"Error: temporary discord outage\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\tests\\notification-scheduler.test.ts:335:34)"},"attempt":1,"durationMs":2} +2026-10-16T12:38:03.546Z error: Job monitor: job failed {"jobId":"notification-10","jobName":"scheduled-notification","error":"temporary discord outage","durationMs":4} +2026-10-16T12:38:03.561Z info: Notification marked for retry or failed {"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:38:05.547Z"} +2026-10-16T12:38:03.577Z info: Scheduler batch complete {"requestId":"test-request-id","processorId":"dispatcher-test","count":1,"durationMs":71} +2026-10-16T12:38:03.604Z info: Fetched due retries {"requestId":"test-request-id","count":1,"processorId":"retry-test-uuid-0001"} +2026-10-16T12:38:03.605Z info: RetryScheduler processing batch {"requestId":"test-request-id","processorId":"retry-test-uuid-0001","count":1} +2026-10-16T12:38:03.605Z info: Retrying notification {"requestId":"test-request-id","id":10,"type":"discord","attempt":2,"maxRetries":3} +2026-10-16T12:38:03.620Z info: Notification marked as completed {"requestId":"test-request-id","id":10} +2026-10-16T12:38:03.632Z info: Retry succeeded {"requestId":"test-request-id","id":10,"attempt":2} +2026-10-16T12:38:03.635Z info: Worker manager force shutdown completed +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +2026-10-16T12:38:03.685Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.699Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T12:38:03.745Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.762Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-1"} +2026-10-16T12:38:03.794Z warn: Recovered stale locks {"count":1} +2026-10-16T12:38:03.846Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.861Z info: Notification marked as completed {"id":13} +2026-10-16T12:38:03.923Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T12:38:03.939Z info: Notification marked for retry or failed {"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:03.986Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.012Z warn: Notification moved to dead letter queue {"id":15,"notificationType":"discord","failureReason":"Test error"} +2026-10-16T12:38:04.014Z info: Notification marked for retry or failed {"id":15,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:04.061Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.077Z info: Notification cancelled {"id":16} +2026-10-16T12:38:04.123Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.137Z info: Notification cancelled {"id":17,"reason":"User requested cancellation"} +2026-10-16T12:38:04.176Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.196Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.231Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} +2026-10-16T12:38:04.247Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.327Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} +2026-10-16T12:38:04.344Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.370Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T12:38:04.388Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.418Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook","version":1} +2026-10-16T12:38:04.430Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} +2026-10-16T12:38:04.435Z info: Database connection closed +2026-10-16T12:38:04.439Z info: Connected to SQLite database {"path":"./data/test-stale-cache.db"} +PASS src/services/retry-policy.test.ts +2026-10-16T12:38:05.185Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:05.186Z info: Database migrations completed +2026-10-16T12:38:05.186Z info: Database initialized successfully {"path":"./data/test-stale-cache.db"} +2026-10-16T12:38:05.203Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:38:05.215Z info: Notification marked as completed {"id":1} +2026-10-16T12:38:05.244Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:38:05.261Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:05.293Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:38:05.321Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Max retries exceeded"} +2026-10-16T12:38:05.321Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T12:38:05.352Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:38:05.364Z info: Fetched and locked pending notifications {"count":1,"processorId":"processor-stale"} +2026-10-16T12:38:05.391Z warn: Recovered stale locks {"count":1} +PASS src/tests/notification-scheduler.test.ts (5.285 s) +2026-10-16T12:38:05.432Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:38:05.446Z info: Notification marked as completed {"id":5} +2026-10-16T12:38:05.449Z info: Database connection closed +ts-jest[ts-compiler] (WARN) src/__tests__/stress.test.ts:631:52 - error +TS2339: Property 'totalSent' does not exist on type '{ timeoutCount: number; acceptedRequests: number; +skippedDuplicates: number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: +number; hitRatio: number; totalChecks: number; }'. + +631 console.log(` Total Sent: ${metrics.totalSent}`); +   ~~~~~~~~~ +src/__tests__/stress.test.ts:632:52 - error TS2339: Property +'duplicatesSkipped' does not exist on type '{ timeoutCount: number; acceptedRequests: number; skippedDuplicates: +number; evictedEntries: number; expiredEntries: number; cacheSize: number; deduplicationWindowMs: number; hitRatio: +number; totalChecks: number; }'. + +632 console.log(` Duplicates Skipped: ${metrics.duplicatesSkipped}`); +   ~~~~~~~~~~~~~~~~~ +2026-10-16T12:38:07.098Z info: Connected to SQLite database {"path":"./data/test-template-api.db"} +2026-10-16T12:38:07.827Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.828Z info: Database migrations completed +2026-10-16T12:38:07.828Z info: Database initialized successfully {"path":"./data/test-template-api.db"} +2026-10-16T12:38:07.830Z error: Database run error {"sql":"duplicate inserts with the same key are silently skipped\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.834Z error: Database run error {"sql":"runMigrations tolerates duplicate-column errors.\r\n-- Dead-letter queue for permanently failed notifications\r\nCREATE TABLE IF NOT EXISTS dead_letter_queue (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scheduled_notification_id INTEGER NOT NULL UNIQUE,\r\n notification_type VARCHAR(50) NOT NULL CHECK (notification_type IN ('discord', 'email', 'webhook', 'sms')),\r\n target_recipient TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n failure_reason TEXT NOT NULL,\r\n error_details TEXT,\r\n created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\r\n last_retried_at DATETIME,\r\n retry_count INTEGER NOT NULL DEFAULT 0 CHECK (retry_count >= 0),\r\n FOREIGN KEY (scheduled_notification_id) REFERENCES scheduled_notifications(id) ON DELETE CASCADE\r\n)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.838Z error: Database run error {"sql":"runMigrations tolerates duplicate-column errors.\r\n-- Trigger to update updated_at timestamp\r\nCREATE TRIGGER IF NOT EXISTS update_scheduled_notifications_timestamp \r\nAFTER UPDATE ON scheduled_notifications\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE scheduled_notifications \r\n SET updated_at = CURRENT_TIMESTAMP \r\n WHERE id = NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.839Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.843Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS update_notification_templates_timestamp\r\nAFTER UPDATE ON notification_templates\r\nFOR EACH ROW\r\nBEGIN\r\n UPDATE notification_templates\r\n SET updated_at = CURRENT_TIMESTAMP\r\n WHERE id = NEW.id","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.844Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.846Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS prevent_template_audit_update\r\nBEFORE UPDATE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.846Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.847Z error: Database run error {"sql":"CREATE TRIGGER IF NOT EXISTS prevent_template_audit_delete\r\nBEFORE DELETE ON notification_template_audit_log\r\nFOR EACH ROW\r\nBEGIN\r\n SELECT RAISE(ABORT, 'Audit records are immutable')","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.848Z error: Database run error {"sql":"END","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.872Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.875Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.877Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.879Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.881Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.883Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.885Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.886Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.888Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.892Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.895Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.896Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.898Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.900Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.901Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.903Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.904Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.906Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.907Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.909Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.911Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.912Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.914Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.916Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.918Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.919Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.921Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.923Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.927Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +FAIL src/tests/template-api-integration.test.ts + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +2026-10-16T12:38:07.929Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.930Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.932Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.933Z error: Database run error {"sql":"DELETE FROM template_usage_log","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:07.936Z info: Database connection closed +2026-10-16T12:38:09.394Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:10.188Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:10.189Z info: Database migrations completed +2026-10-16T12:38:10.189Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:10.211Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:38:10.248Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T12:38:10.296Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"Attempt 2 failed"} +2026-10-16T12:38:10.298Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T12:38:10.344Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"Final failure"} +2026-10-16T12:38:10.345Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T12:38:10.351Z info: Database connection closed +2026-10-16T12:38:10.464Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:11.450Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:11.451Z info: Database migrations completed +2026-10-16T12:38:11.451Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:11.468Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:38:11.508Z info: Notification marked as completed {"id":1} +2026-10-16T12:38:11.517Z info: Database connection closed +2026-10-16T12:38:11.627Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:12.531Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:12.532Z info: Database migrations completed +2026-10-16T12:38:12.532Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:12.581Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:38:12.738Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T12:38:13.114Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"Second attempt failed"} +2026-10-16T12:38:13.115Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T12:38:13.240Z info: Notification marked as completed {"id":1} +2026-10-16T12:38:13.286Z info: Database connection closed +2026-10-16T12:38:13.457Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:14.651Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:14.652Z info: Database migrations completed +2026-10-16T12:38:14.652Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:14.686Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:38:14.734Z info: Notification marked as completed {"id":1} +2026-10-16T12:38:14.748Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:38:14.805Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:14.847Z info: Notification marked as completed {"id":2} +2026-10-16T12:38:14.871Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:38:14.901Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:14.952Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:14.990Z info: Notification marked as completed {"id":3} +2026-10-16T12:38:15.018Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.051Z info: Notification marked for retry or failed {"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:15.087Z info: Notification marked for retry or failed {"id":4,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:15.149Z warn: Notification moved to dead letter queue {"id":4,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:15.150Z info: Notification marked for retry or failed {"id":4,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:15.202Z warn: Notification moved to dead letter queue {"id":4,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:15.203Z info: Notification marked for retry or failed {"id":4,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:15.220Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.256Z info: Notification marked as completed {"id":5} +2026-10-16T12:38:15.279Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.333Z info: Notification marked for retry or failed {"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:15.381Z info: Notification marked as completed {"id":6} +2026-10-16T12:38:15.401Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.448Z info: Notification marked for retry or failed {"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:15.484Z info: Notification marked for retry or failed {"id":7,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:15.533Z info: Notification marked as completed {"id":7} +2026-10-16T12:38:15.555Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.597Z info: Notification marked for retry or failed {"id":8,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:15.631Z info: Notification marked for retry or failed {"id":8,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:15.687Z warn: Notification moved to dead letter queue {"id":8,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:15.688Z info: Notification marked for retry or failed {"id":8,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:15.764Z warn: Notification moved to dead letter queue {"id":8,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:15.765Z info: Notification marked for retry or failed {"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:15.789Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.880Z info: Notification marked as completed {"id":9} +2026-10-16T12:38:15.905Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T12:38:15.944Z info: Notification marked for retry or failed {"id":10,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:15.980Z info: Notification marked as completed {"id":10} +2026-10-16T12:38:16.006Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T12:38:16.098Z info: Notification marked for retry or failed {"id":11,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:16.137Z info: Notification marked for retry or failed {"id":11,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:16.199Z info: Notification marked as completed {"id":11} +2026-10-16T12:38:16.221Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T12:38:16.277Z info: Notification marked for retry or failed {"id":12,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:16.333Z info: Notification marked for retry or failed {"id":12,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:16.395Z warn: Notification moved to dead letter queue {"id":12,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:16.395Z info: Notification marked for retry or failed {"id":12,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:16.447Z warn: Notification moved to dead letter queue {"id":12,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:16.448Z info: Notification marked for retry or failed {"id":12,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:16.469Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T12:38:16.498Z info: Notification marked as completed {"id":13} +2026-10-16T12:38:16.514Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T12:38:16.544Z info: Notification marked for retry or failed {"id":14,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:16.671Z info: Notification marked as completed {"id":14} +2026-10-16T12:38:16.698Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T12:38:16.751Z info: Notification marked for retry or failed {"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:16.790Z info: Notification marked for retry or failed {"id":15,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:16.831Z info: Notification marked as completed {"id":15} +2026-10-16T12:38:16.851Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T12:38:16.935Z info: Notification marked for retry or failed {"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:17.039Z info: Notification marked for retry or failed {"id":16,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:17.132Z warn: Notification moved to dead letter queue {"id":16,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:17.132Z info: Notification marked for retry or failed {"id":16,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:17.181Z warn: Notification moved to dead letter queue {"id":16,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:17.182Z info: Notification marked for retry or failed {"id":16,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:17.205Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T12:38:17.246Z info: Notification marked as completed {"id":17} +2026-10-16T12:38:17.265Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T12:38:17.314Z info: Notification marked for retry or failed {"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:17.354Z info: Notification marked as completed {"id":18} +2026-10-16T12:38:17.370Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T12:38:17.399Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:17.488Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:17.583Z info: Notification marked as completed {"id":19} +2026-10-16T12:38:17.598Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} +2026-10-16T12:38:17.653Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:17.821Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:18.122Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:18.123Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:18.228Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:18.229Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:18.247Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"discord"} +2026-10-16T12:38:18.280Z info: Notification marked as completed {"id":21} +2026-10-16T12:38:18.337Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"discord"} +2026-10-16T12:38:18.405Z info: Notification marked for retry or failed {"id":22,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:19.327Z info: Notification marked as completed {"id":22} +2026-10-16T12:38:19.345Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} +2026-10-16T12:38:19.380Z info: Notification marked for retry or failed {"id":23,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:19.418Z info: Notification marked for retry or failed {"id":23,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:19.451Z info: Notification marked as completed {"id":23} +2026-10-16T12:38:19.467Z info: Scheduled notification created {"id":24,"executeAt":{},"type":"discord"} +2026-10-16T12:38:19.497Z info: Notification marked for retry or failed {"id":24,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:19.527Z info: Notification marked for retry or failed {"id":24,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:19.568Z warn: Notification moved to dead letter queue {"id":24,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:19.568Z info: Notification marked for retry or failed {"id":24,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:19.611Z warn: Notification moved to dead letter queue {"id":24,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:19.613Z info: Notification marked for retry or failed {"id":24,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:19.631Z info: Scheduled notification created {"id":25,"executeAt":{},"type":"discord"} +2026-10-16T12:38:19.665Z info: Notification marked as completed {"id":25} +2026-10-16T12:38:19.666Z info: Database connection closed +2026-10-16T12:38:19.667Z error: Database run error {"sql":"\n INSERT INTO scheduled_notifications (\n payload, payload_hash, notification_type, target_recipient, execute_at, expires_at,\n created_at, updated_at, status, retry_count, max_retries, processing_started_at,\n processing_completed_at, processor_id, lock_expires_at, last_error, error_details,\n event_id, contract_address, priority, metadata, next_retry_at, deduplication_key\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ","params":["{\"test\":\"batch-25\"}",null,"discord","webhook-url","2026-10-16T12:38:19.665Z",null,"2026-10-16T12:38:19.665Z","2026-10-16T12:38:19.665Z","PENDING",0,3,null,null,null,null,null,null,null,null,5,null,null,null],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T12:38:19.668Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T12:38:19.786Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:19.788Z warn: Database connection recovered {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:19.789Z error: Database run error {"sql":"\n INSERT INTO scheduled_notifications (\n payload, payload_hash, notification_type, target_recipient, execute_at, expires_at,\n created_at, updated_at, status, retry_count, max_retries, processing_started_at,\n processing_completed_at, processor_id, lock_expires_at, last_error, error_details,\n event_id, contract_address, priority, metadata, next_retry_at, deduplication_key\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ","params":["{\"test\":\"batch-25\"}",null,"discord","webhook-url","2026-10-16T12:38:19.665Z",null,"2026-10-16T12:38:19.665Z","2026-10-16T12:38:19.665Z","PENDING",0,3,null,null,null,null,null,null,null,null,5,null,null,null],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:19.802Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:20.563Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:20.564Z info: Database migrations completed +2026-10-16T12:38:20.564Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:20.580Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:38:20.614Z info: Notification marked as completed {"id":1} +2026-10-16T12:38:20.632Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:38:20.650Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:38:20.671Z info: Fetched and locked pending notifications {"count":1,"processorId":"test-processor"} +2026-10-16T12:38:20.673Z info: Database connection closed +2026-10-16T12:38:20.800Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:20.822Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:20.822Z info: Database migrations completed +2026-10-16T12:38:20.823Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:20.845Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:38:20.872Z info: Notification marked as completed {"id":4} +2026-10-16T12:38:20.890Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:38:20.912Z info: Notification cancelled {"id":5} +2026-10-16T12:38:20.915Z info: Database connection closed +2026-10-16T12:38:21.031Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.057Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:21.066Z info: Database migrations completed +2026-10-16T12:38:21.067Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.085Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T12:38:21.102Z info: Notification marked as completed {"id":6} +2026-10-16T12:38:21.105Z info: Database connection closed +2026-10-16T12:38:21.219Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.235Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:21.236Z info: Database migrations completed +2026-10-16T12:38:21.236Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.251Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T12:38:21.278Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T12:38:21.332Z info: Notification marked as completed {"id":7} +2026-10-16T12:38:21.370Z info: Notification marked as completed {"id":8} +2026-10-16T12:38:21.373Z info: Database connection closed +2026-10-16T12:38:21.482Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.500Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:21.500Z info: Database migrations completed +2026-10-16T12:38:21.501Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.516Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T12:38:21.539Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":1,"maxRetries":9} +2026-10-16T12:38:21.569Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":2,"maxRetries":9} +2026-10-16T12:38:21.595Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":3,"maxRetries":9} +2026-10-16T12:38:21.621Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":4,"maxRetries":9} +2026-10-16T12:38:21.651Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":5,"maxRetries":9} +2026-10-16T12:38:21.677Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":6,"maxRetries":9} +2026-10-16T12:38:21.704Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":7,"maxRetries":9} +2026-10-16T12:38:21.731Z info: Notification marked for retry or failed {"id":9,"newStatus":"PENDING","retryCount":8,"maxRetries":9} +2026-10-16T12:38:21.778Z warn: Notification moved to dead letter queue {"id":9,"notificationType":"discord","failureReason":"Attempt 9 failed"} +2026-10-16T12:38:21.779Z info: Notification marked for retry or failed {"id":9,"newStatus":"DEAD_LETTERED","retryCount":9,"maxRetries":9} +2026-10-16T12:38:21.810Z info: Notification marked as completed {"id":9} +2026-10-16T12:38:21.814Z info: Database connection closed +2026-10-16T12:38:21.934Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.953Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:38:21.954Z info: Database migrations completed +2026-10-16T12:38:21.954Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-retry-dedup.db"} +2026-10-16T12:38:21.970Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T12:38:21.995Z info: Notification marked as completed {"id":10} +2026-10-16T12:38:22.007Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.033Z info: Notification marked as completed {"id":11} +2026-10-16T12:38:22.047Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.073Z info: Notification marked as completed {"id":12} +2026-10-16T12:38:22.088Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.115Z info: Notification marked as completed {"id":13} +2026-10-16T12:38:22.129Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.162Z info: Notification marked as completed {"id":14} +2026-10-16T12:38:22.248Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.277Z info: Notification marked for retry or failed {"id":15,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:22.300Z info: Notification marked as completed {"id":15} +2026-10-16T12:38:22.314Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.340Z info: Notification marked for retry or failed {"id":16,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:22.364Z info: Notification marked as completed {"id":16} +2026-10-16T12:38:22.378Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.406Z info: Notification marked for retry or failed {"id":17,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:22.435Z info: Notification marked as completed {"id":17} +2026-10-16T12:38:22.450Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.479Z info: Notification marked for retry or failed {"id":18,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:22.505Z info: Notification marked for retry or failed {"id":18,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:22.533Z info: Notification marked as completed {"id":18} +2026-10-16T12:38:22.550Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.578Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:22.629Z info: Notification marked for retry or failed {"id":19,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:22.662Z info: Notification marked as completed {"id":19} +2026-10-16T12:38:22.685Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"discord"} +2026-10-16T12:38:22.720Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:38:22.755Z info: Notification marked for retry or failed {"id":20,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:38:22.812Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 3 failed"} +2026-10-16T12:38:22.813Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:38:22.859Z warn: Notification moved to dead letter queue {"id":20,"notificationType":"discord","failureReason":"Attempt 4 failed"} +2026-10-16T12:38:22.860Z info: Notification marked for retry or failed {"id":20,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} +2026-10-16T12:38:22.865Z info: Database connection closed +FAIL src/services/retry-deduplication.test.ts (15.247 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +PASS src/services/retry-scheduler-webhook.test.ts +ts-jest[ts-compiler] (WARN) src/__tests__/smoke/notification-pipeline.smoke.test.ts:602:5 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +602 contractId: contractAddress, +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +2026-10-16T12:38:26.817Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T12:38:26.818Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T12:38:26.818Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T12:38:26.819Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +2026-10-16T12:38:26.819Z warn: Event registry at capacity, evicting oldest events {"maxEvents":5,"evicted":1} +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/discord-notification.test.ts (5.658 s) + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +ts-jest[ts-compiler] (WARN) src/services/notification-expiration.test.ts:40:9 - +error TS2322: Type 'string' is not assignable to type 'Contract'. + +40 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:45:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +45 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:46:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +46 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:63:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +63 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:68:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +68 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:69:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +69 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:86:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +86 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:91:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +91 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:92:18 - error TS2353: Object +literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +92 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:116:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +116 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:121:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +121 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:122:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +122 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:140:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +140 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:145:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +145 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:146:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +146 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:164:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +164 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:169:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +169 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:170:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +170 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:199:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +199 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:204:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +204 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:205:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +205 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:222:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +222 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:227:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +227 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:228:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +228 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:340:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +340 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:345:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +345 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:346:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +346 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:379:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +379 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:384:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +384 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:385:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +385 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:407:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +407 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:412:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +412 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:413:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +413 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:435:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +435 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:440:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +440 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:441:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +441 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:467:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +467 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:472:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +472 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:473:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +473 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:503:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +503 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:508:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +508 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:509:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +509 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:521:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +521 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:526:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +526 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:527:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +527 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:552:9 - error TS2322: Type +'string' is not assignable to type 'Contract'. + +552 contractId: 'test-contract', +   ~~~~~~~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:252:9 + 252 contractId?: Contract; +    ~~~~~~~~~~ + The expected type comes from property 'contractId' which is declared here on type 'EventResponse' +src/services/notification-expiration.test.ts:557:17 - error TS2322: Type +'string' is not assignable to type 'ScVal'. + +557 topic: ['topic'], +   ~~~~~~~ +src/services/notification-expiration.test.ts:558:18 - error TS2353: +Object literal may only specify known properties, and 'type' does not exist in type 'ScVal'. + +558 value: { type: 'i128', b64: 'value' }, +   ~~~~ + + node_modules/@stellar/stellar-sdk/lib/rpc/api.d.ts:254:9 + 254 value: xdr.ScVal; +    ~~~~~ + The expected type comes from property 'value' which is declared here on type 'EventResponse' +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +PASS src/services/event-subscriber-checkpoint.test.ts +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +PASS src/services/event-processing-queue.test.ts +PASS src/services/dns-rebinding-guard.test.ts +ts-jest[ts-compiler] (WARN) src/services/archive.test.ts:237:7 - error +TS2741: Property 'expiresAt' is missing in type '{ originalId: number; payload: string; notificationType: string; +targetRecipient: string; executeAt: string; createdAt: string; processingCompletedAt: string; status: string; +retryCount: number; lastError: null; eventId: null; contractAddress: null; metadata: null; }' but required in type '{ +originalId: number; payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: +string | null; createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +237 { +   ~ +238 originalId: 1, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +250 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +251 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +src/services/archive.test.ts:252:7 - error TS2741: Property 'expiresAt' +is missing in type '{ originalId: number; payload: string; notificationType: string; targetRecipient: string; +executeAt: string; createdAt: string; processingCompletedAt: null; status: string; retryCount: number; lastError: +string; eventId: string; contractAddress: string; metadata: null; }' but required in type '{ originalId: number; +payload: string; notificationType: string; targetRecipient: string; executeAt: string; expiresAt: string | null; +createdAt: string; processingCompletedAt: string | null; ... 5 more ...; metadata: string | null; }'. + +252 { +   ~ +253 originalId: 2, +  ~~~~~~~~~~~~~~~~~~~~~~ +... +265 metadata: null, +  ~~~~~~~~~~~~~~~~~~~~~~~ +266 }, +  ~~~~~~~ + + src/services/archive-store.ts:105:7 + 105 expiresAt: string | null; +    ~~~~~~~~~ + 'expiresAt' is declared here. +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +2026-10-16T12:38:40.716Z info: ArchiveService: archived notifications {"count":2,"cutoff":"2026-10-09T12:38:40.715Z","statusDistribution":{"completed":1,"failed":1,"cancelled":0,"expired":0}} +2026-10-16T12:38:40.718Z info: ArchiveService: cycle complete {"archived":2,"purged":0,"durationMs":3,"jobId":"archive-cycle-1792154320714"} +2026-10-16T12:38:40.719Z info: Worker manager force shutdown completed +2026-10-16T12:38:40.721Z info: ArchiveService: archived processed notification immediately {"id":42} +2026-10-16T12:38:40.726Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":2,"jobId":"archive-cycle-1792154320724"} +2026-10-16T12:38:40.727Z info: Worker manager force shutdown completed +2026-10-16T12:38:40.729Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792154320728"} +2026-10-16T12:38:40.730Z info: Worker manager force shutdown completed +2026-10-16T12:38:40.732Z info: ArchiveService: purged expired archive records {"count":2,"cutoff":"2026-07-18T12:38:40.731Z"} +2026-10-16T12:38:40.732Z info: ArchiveService: cycle complete {"archived":0,"purged":2,"durationMs":1,"jobId":"archive-cycle-1792154320731"} +2026-10-16T12:38:40.733Z info: Worker manager force shutdown completed +2026-10-16T12:38:40.735Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":1,"jobId":"archive-cycle-1792154320734"} +2026-10-16T12:38:40.735Z info: Worker manager force shutdown completed +2026-10-16T12:38:40.736Z info: ArchiveService started {"intervalMs":60000,"archiveAfterMs":604800000,"deleteAfterMs":7776000000,"batchSize":500} +2026-10-16T12:38:40.736Z info: Waiting for active archival jobs to complete {"activeJobs":1} +2026-10-16T12:38:40.736Z info: Initiating graceful shutdown {"activeJobs":1} +2026-10-16T12:38:40.740Z info: ArchiveService: cycle complete {"archived":0,"purged":0,"durationMs":4,"jobId":"archive-cycle-1792154320736"} +2026-10-16T12:38:40.741Z info: Worker manager force shutdown completed +2026-10-16T12:38:40.745Z info: Handling GET /api/archive {"requestId":"req-2"} +2026-10-16T12:38:40.756Z info: Handling GET /api/archive/:id {"requestId":"req-3","id":999} +2026-10-16T12:38:40.758Z info: Handling GET /api/archive/:id {"requestId":"req-4","id":42} +2026-10-16T12:38:40.762Z info: Handling POST /api/archive/run {"requestId":"req-6"} +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +PASS src/api/rate-limiter.test.ts +PASS src/services/stellar-rpc-manager.test.ts (9.974 s) +PASS src/api/rate-limit-scenarios.test.ts +ts-jest[ts-compiler] (WARN) src/api/__tests__/api-contract.test.ts:20:46 - +error TS2345: Argument of type '{ status: string; }' is not assignable to parameter of type 'never'. + +20 getHealth: jest.fn().mockResolvedValue({ status: 'healthy' }), +   ~~~~~~~~~~~~~~~~~~~~~ +src/api/__tests__/api-contract.test.ts:60:38 - error TS2345: Argument of +type '{ ok: number; }' is not assignable to parameter of type 'never'. + +60 get: jest.fn().mockResolvedValue({ ok: 1 }), +   ~~~~~~~~~ +PASS src/api/__tests__/api-contract.test.ts +ts-jest[ts-compiler] (WARN) +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:164:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +164 { baseDelayMs: 1000, maxRetries: 3, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +src/__tests__/notification-delivery-lifecycle.e2e.test.ts:203:11 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +203 { baseDelayMs: 1000, maxRetries: 1, processIntervalMs: 1000 } +   ~~~~~~~~~~~ +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (11.415 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +ts-jest[ts-compiler] (WARN) src/tests/notification-scheduler-refactored.test.ts:309:13 - +error TS2451: Cannot redeclare block-scoped variable 'pastLock'. + +309 const pastLock = NotificationFixtureBuilder.dates.past(1000); +   ~~~~~~~~ +src/tests/notification-scheduler-refactored.test.ts:334:13 - error TS2451: +Cannot redeclare block-scoped variable 'pastLock'. + +334 const pastLock = NotificationFixtureBuilder.dates.past(1000); +   ~~~~~~~~ +FAIL src/tests/notification-scheduler-refactored.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\tests\notification-scheduler-refactored.test.ts:271 + const pastLock = notification_fixture_builder_1.NotificationFixtureBuilder.dates.past(1000); + ^ + + SyntaxError: Identifier 'pastLock' has already been declared + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +2026-10-16T12:39:19.351Z info: Migration 004 (database-data-integrity) applied successfully +2026-10-16T12:39:19.635Z info: Migration 004-repeat (database-data-integrity) applied successfully +2026-10-16T12:39:19.968Z error: Migration 004 failed, rolling back {"error":{"message":"Migration 004 aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 row(s)","name":"Error","stack":"Error: Migration 004 aborted before rebuilding tables. Repair invalid legacy data first: processed_events: 1 row(s)\n at assertLegacyDataIsValid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:372:11)\n at Object.up (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.ts:382:5)\n at MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\004-database-data-integrity.test.ts:281:5)"}} +PASS src/migrations/004-database-data-integrity.test.ts +2026-10-16T12:39:20.171Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:39:20.188Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:39:20.189Z info: Database migrations completed +2026-10-16T12:39:20.189Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:39:20.229Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n (payload, notification_type, target_recipient, execute_at, status)\n VALUES ('{}', 'discord', 'user-1', '2026-01-01', 'UNKNOWN')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:39:20.231Z info: Database connection closed +FAIL src/services/discord-notification-refactored.test.ts (80.619 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +2026-10-16T12:40:44.183Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:45.086Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:45.087Z info: Database migrations completed +2026-10-16T12:40:45.087Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:45.123Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:40:45.149Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:40:45.182Z info: Notification marked as completed {"id":2} +2026-10-16T12:40:45.275Z info: Database connection closed +2026-10-16T12:40:45.292Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:46.233Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:46.234Z info: Database migrations completed +2026-10-16T12:40:46.234Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:46.246Z info: Database connection closed +2026-10-16T12:40:46.251Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:47.559Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:47.560Z info: Database migrations completed +2026-10-16T12:40:47.561Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:47.587Z info: Handling GET /api/schedule/queue {"requestId":"a63d57d9","correlationId":"0e5425fe-4931-443f-b3c8-0399a6a46306","count":0,"durationMs":13} +2026-10-16T12:40:47.637Z info: Database connection closed +2026-10-16T12:40:47.658Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:49.757Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:49.772Z info: Database migrations completed +2026-10-16T12:40:49.773Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:49.842Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:40:49.880Z info: Handling GET /api/schedule/queue {"requestId":"b84e3943","correlationId":"42f3edae-d629-47aa-b8a0-f855d0a6d62f","count":1,"durationMs":11} +2026-10-16T12:40:49.907Z info: Database connection closed +2026-10-16T12:40:49.923Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:51.456Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:51.465Z info: Database migrations completed +2026-10-16T12:40:51.466Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:52.059Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:40:52.103Z info: Notification marked as completed {"id":1} +2026-10-16T12:40:52.136Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:40:52.624Z warn: Notification moved to dead letter queue {"id":2,"notificationType":"discord","failureReason":"oops"} +2026-10-16T12:40:52.639Z info: Notification marked for retry or failed {"id":2,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1} +2026-10-16T12:40:52.667Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:40:52.970Z info: Handling GET /api/schedule/queue {"requestId":"e8266d9e","correlationId":"3af3de5b-1b17-40a4-bdf3-be5c21351455","count":1,"durationMs":46} +2026-10-16T12:40:53.273Z info: Database connection closed +2026-10-16T12:40:53.726Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:55.065Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:55.076Z info: Database migrations completed +2026-10-16T12:40:55.079Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:55.392Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:40:55.488Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:40:55.522Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:40:55.607Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:40:55.632Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:40:55.734Z info: Handling GET /api/schedule/queue {"requestId":"f9510b46","correlationId":"1f24d150-b4c1-42c0-ae37-2e405d382f8a","count":3,"durationMs":44} +2026-10-16T12:40:55.803Z info: Database connection closed +2026-10-16T12:40:55.882Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:57.050Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:57.052Z info: Database migrations completed +2026-10-16T12:40:57.052Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:57.204Z info: Database connection closed +2026-10-16T12:40:57.220Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:58.241Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:58.242Z info: Database migrations completed +2026-10-16T12:40:58.242Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:58.274Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:40:58.284Z info: Handling GET /api/schedule/queue {"requestId":"5bbb9bc1","correlationId":"0cd3d39d-97c8-43eb-a128-dbfaa7dca68c","count":1,"durationMs":3} +2026-10-16T12:40:58.296Z info: Database connection closed +2026-10-16T12:40:58.308Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:59.485Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:40:59.487Z info: Database migrations completed +2026-10-16T12:40:59.487Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:40:59.538Z info: Database connection closed +2026-10-16T12:40:59.550Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:41:00.649Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:00.650Z info: Database migrations completed +2026-10-16T12:41:00.651Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:41:00.719Z info: NotificationHealthMonitor started {"intervalMs":60000} +2026-10-16T12:41:00.751Z info: NotificationHealthMonitor stopped +2026-10-16T12:41:00.769Z info: Database connection closed +2026-10-16T12:41:00.771Z error: Database get error {"sql":"SELECT COUNT(*) as count FROM dead_letter_queue","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T12:41:00.773Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T12:41:00.791Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:41:00.905Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:41:00.923Z warn: Database connection recovered {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +PASS src/api/queue-visibility.test.ts (20.439 s) +2026-10-16T12:41:02.000Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:02.001Z info: Database migrations completed +2026-10-16T12:41:02.001Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-visibility.db"} +2026-10-16T12:41:02.016Z info: NotificationHealthMonitor started {"intervalMs":60000} +2026-10-16T12:41:02.028Z info: NotificationHealthMonitor stopped +2026-10-16T12:41:02.038Z info: Database connection closed +PASS src/services/event-deduplication-service.test.ts +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +ts-jest[ts-compiler] (WARN) src/services/concurrent-event-processing.test.ts:74:40 - +error TS2352: Conversion of type '{ address: string; name: string; }' to type 'ContractConfig' may +be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the +expression to 'unknown' first. + Property 'events' is missing in type '{ address: string; name: string; }' but required in type 'ContractConfig'. + +74 const contractConfig: ContractConfig = { address: CONTRACT, name: 'Concurrency' } as ContractConfig; +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/types/index.ts:18:3 + 18 events: string[]; +    ~~~~~~ + 'events' is declared here. +FAIL src/__tests__/stress.test.ts (195.991 s) + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + +2026-10-16T12:41:22.952Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:23.686Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:23.687Z info: Database migrations completed +2026-10-16T12:41:23.688Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:23.710Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:23.751Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:41:23.783Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:41:23.816Z info: Notification marked as completed {"id":1} +2026-10-16T12:41:23.824Z info: Database connection closed +2026-10-16T12:41:23.931Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:24.924Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:24.925Z info: Database migrations completed +2026-10-16T12:41:24.925Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:24.943Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:24.972Z info: Notification marked as completed {"id":1} +2026-10-16T12:41:24.993Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:41:25.019Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:41:25.052Z info: Notification marked as completed {"id":2} +2026-10-16T12:41:25.071Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:41:25.100Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T12:41:25.147Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 2"} +2026-10-16T12:41:25.148Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T12:41:25.205Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 3"} +2026-10-16T12:41:25.206Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T12:41:25.210Z info: Database connection closed +2026-10-16T12:41:25.321Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:26.243Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:26.244Z info: Database migrations completed +2026-10-16T12:41:26.244Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:26.263Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:26.292Z info: Notification marked as completed {"id":1} +2026-10-16T12:41:26.307Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:41:26.344Z info: Notification marked as completed {"id":2} +2026-10-16T12:41:26.368Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:41:26.407Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:41:26.438Z info: Notification marked as completed {"id":3} +2026-10-16T12:41:26.454Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:41:26.490Z info: Notification marked for retry or failed {"id":4,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:41:26.526Z info: Notification marked as completed {"id":4} +2026-10-16T12:41:26.546Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:41:26.581Z info: Notification marked for retry or failed {"id":5,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:41:26.610Z info: Notification marked as completed {"id":5} +2026-10-16T12:41:26.626Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T12:41:26.650Z info: Notification marked for retry or failed {"id":6,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T12:41:26.693Z warn: Notification moved to dead letter queue {"id":6,"notificationType":"discord","failureReason":"Error 2"} +2026-10-16T12:41:26.694Z info: Notification marked for retry or failed {"id":6,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T12:41:26.735Z warn: Notification moved to dead letter queue {"id":6,"notificationType":"discord","failureReason":"Error 3"} +2026-10-16T12:41:26.736Z info: Notification marked for retry or failed {"id":6,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T12:41:26.742Z info: Database connection closed +2026-10-16T12:41:26.861Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:28.679Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:28.680Z info: Database migrations completed +2026-10-16T12:41:28.680Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:28.697Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:28.726Z info: Notification marked as completed {"id":1} +2026-10-16T12:41:28.752Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:41:28.784Z info: Notification marked as completed {"id":2} +2026-10-16T12:41:28.799Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:41:28.843Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error"} +2026-10-16T12:41:28.843Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":0} +2026-10-16T12:41:28.849Z info: Database connection closed +2026-10-16T12:41:28.967Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:30.845Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:30.846Z info: Database migrations completed +2026-10-16T12:41:30.847Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:30.852Z info: Database connection closed +2026-10-16T12:41:30.972Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +FAIL src/services/concurrent-event-processing.test.ts (22.51 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +2026-10-16T12:41:31.852Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:31.852Z info: Database migrations completed +2026-10-16T12:41:31.852Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-execution-metrics.db"} +2026-10-16T12:41:31.867Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:31.880Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:41:31.905Z info: Notification marked as completed {"id":2} +2026-10-16T12:41:31.908Z info: Database connection closed +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/execution-metrics.test.ts (10.666 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +PASS src/utils/ledger-range-validator.test.ts +PASS src/utils/redact.test.ts +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-reorg.test.ts:62:3 - +error TS2353: Object literal may only specify known properties, and 'maxPayloadSizeBytes' does not +exist in type 'Config'. + +62 maxPayloadSizeBytes: 64 * 1024, +   ~~~~~~~~~~~~~~~~~~~ +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "4d99eb21"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "ea0aaea1-ea79-4b33-8f3f-94d4ba69a2ff", +"eventId": "event-1", "requestId": "ea0aaea1-ea79-4b33-8f3f-94d4ba69a2ff"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +2026-10-16T12:41:36.003Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154495997-1.db"} +2026-10-16T12:41:36.809Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:36.810Z info: Database migrations completed +2026-10-16T12:41:36.810Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154495997-1.db"} +2026-10-16T12:41:36.830Z info: Database connection closed +2026-10-16T12:41:36.838Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154496835-2.db"} +2026-10-16T12:41:37.609Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:37.609Z info: Database migrations completed +2026-10-16T12:41:37.610Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154496835-2.db"} +2026-10-16T12:41:37.631Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T12:41:37.646Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:41:37.653Z info: Database connection closed +2026-10-16T12:41:37.660Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154497658-3.db"} +2026-10-16T12:41:39.448Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:39.450Z info: Database migrations completed +2026-10-16T12:41:39.452Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154497658-3.db"} +2026-10-16T12:41:39.491Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T12:41:39.520Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T12:41:39.531Z info: Database connection closed +2026-10-16T12:41:39.540Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154499537-4.db"} +2026-10-16T12:41:40.670Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:40.671Z info: Database migrations completed +2026-10-16T12:41:40.671Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154499537-4.db"} +2026-10-16T12:41:40.692Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:40.707Z info: Notification marked as completed {"id":1} +2026-10-16T12:41:40.717Z info: Database connection closed +2026-10-16T12:41:40.721Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154500719-5.db"} +2026-10-16T12:41:41.515Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:41.517Z info: Database migrations completed +2026-10-16T12:41:41.518Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154500719-5.db"} +2026-10-16T12:41:41.558Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T12:41:41.611Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T12:41:41.723Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"webhook","failureReason":"Permanent failure"} +2026-10-16T12:41:41.725Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T12:41:41.769Z info: Database connection closed +2026-10-16T12:41:41.777Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154501774-6.db"} +2026-10-16T12:41:42.766Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:42.767Z info: Database migrations completed +2026-10-16T12:41:42.767Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154501774-6.db"} +2026-10-16T12:41:42.775Z info: Database connection closed +2026-10-16T12:41:42.783Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154502779-7.db"} +2026-10-16T12:41:43.627Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:43.632Z info: Database migrations completed +2026-10-16T12:41:43.633Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154502779-7.db"} +2026-10-16T12:41:43.656Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:43.673Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:41:43.687Z info: Notification marked as completed {"id":2} +2026-10-16T12:41:43.711Z info: Handling GET /api/schedule/queue/metrics {"requestId":"9c229aae","correlationId":"6e9f9cbe-c7fb-4334-a9f3-805ec3d898ab","durationMs":6} +2026-10-16T12:41:43.721Z info: Database connection closed +2026-10-16T12:41:43.726Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154503723-8.db"} +2026-10-16T12:41:45.476Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:45.477Z info: Database migrations completed +2026-10-16T12:41:45.477Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154503723-8.db"} +2026-10-16T12:41:45.526Z info: Handling GET /api/schedule/queue/metrics {"requestId":"8b64ce56","correlationId":"efde34f2-0f63-45c2-8532-90d9793cb3bf","durationMs":1} +2026-10-16T12:41:45.586Z info: Handling GET /api/schedule/queue/metrics {"requestId":"57e8b39e","correlationId":"dd9889fd-d6eb-4444-bcc5-f108692f04ee","durationMs":1} +2026-10-16T12:41:45.610Z info: Database connection closed +2026-10-16T12:41:45.643Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154505635-9.db"} +2026-10-16T12:41:46.821Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:46.825Z info: Database migrations completed +2026-10-16T12:41:46.825Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154505635-9.db"} +2026-10-16T12:41:46.870Z info: Database connection closed +2026-10-16T12:41:46.889Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154506877-10.db"} +2026-10-16T12:41:47.768Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:47.769Z info: Database migrations completed +2026-10-16T12:41:47.770Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154506877-10.db"} +2026-10-16T12:41:47.788Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:41:47.875Z info: Database connection closed +2026-10-16T12:41:47.899Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154507881-11.db"} +2026-10-16T12:41:49.068Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:49.069Z info: Database migrations completed +2026-10-16T12:41:49.070Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-queue-operational-metrics-1792154507881-11.db"} +2026-10-16T12:41:49.094Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T12:41:49.120Z info: NotificationHealthMonitor started {"intervalMs":60000} +2026-10-16T12:41:49.132Z info: NotificationHealthMonitor stopped +2026-10-16T12:41:49.141Z info: Database connection closed +FAIL src/api/queue-operational-metrics.test.ts (15.625 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +2026-10-16T12:41:52.158Z info: Migration 005 (notification-expiration) applied successfully +2026-10-16T12:41:52.243Z info: Migration 005-repeat (notification-expiration) applied successfully +PASS src/migrations/005-notification-expiration.test.ts +2026-10-16T12:41:52.290Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:296:5)"}} +2026-10-16T12:41:52.309Z error: Migration 005 failed, rolling back {"error":{"message":"Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)","name":"Error","stack":"Error: Migration 005 aborted before rebuilding tables. Repair invalid legacy data first: scheduled_notifications: 1 row(s)\n at auditLegacyRows (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:101:11)\n at Object.up (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.ts:111:5)\n at MigrationRunner.applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\database\\migration-system.ts:100:7)\n at applyMigration (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:144:3)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\migrations\\005-notification-expiration.test.ts:310:5)"}} +PASS src/test-utils/notification-fixture-builder.test.ts +PASS src/api/events-server.history.test.ts +2026-10-16T12:41:56.694Z info: Connected to SQLite database {"path":"./data/test-templates.db"} +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +2026-10-16T12:41:57.743Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:57.753Z info: Database migrations completed +2026-10-16T12:41:57.756Z info: Database initialized successfully {"path":"./data/test-templates.db"} +2026-10-16T12:41:57.770Z error: Database run error {"sql":"-- Indexes for performance\r\nCREATE INDEX IF NOT EXISTS idx_templates_unique_key \r\n ON notification_templates(unique_key)","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:41:57.801Z info: Database connection closed +PASS src/utils/event-utils.test.ts +2026-10-16T12:42:02.382Z info: Worker manager force shutdown completed +PASS src/services/database-cleanup-job.test.ts +2026-10-16T12:42:02.840Z info: Worker manager force shutdown completed +2026-10-16T12:42:02.861Z info: Worker manager force shutdown completed +2026-10-16T12:42:02.897Z info: Worker manager force shutdown completed +PASS src/api/api-version-registry.test.ts +2026-10-16T12:42:05.026Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} +2026-10-16T12:42:05.073Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} +2026-10-16T12:42:05.075Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/abc","version":1} +2026-10-16T12:42:05.094Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} +2026-10-16T12:42:05.099Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} +2026-10-16T12:42:05.119Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook","version":1} +PASS src/services/notification-api.test.ts +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 0, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +PASS src/services/notification-analytics-aggregator.test.ts +2026-10-16T12:42:08.387Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:09.153Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:09.154Z info: Database migrations completed +2026-10-16T12:42:09.154Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:09.171Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:09.173Z info: Database connection closed +2026-10-16T12:42:09.176Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:09.192Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:09.193Z info: Database migrations completed +2026-10-16T12:42:09.193Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:09.209Z info: Fetched and locked pending notifications {"count":1,"processorId":"restarted-worker"} +2026-10-16T12:42:09.214Z info: Database connection closed +2026-10-16T12:42:09.221Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (34.093 s) + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +2026-10-16T12:42:10.123Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:10.124Z info: Database migrations completed +2026-10-16T12:42:10.124Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:10.156Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:10.175Z info: Fetched and locked pending notifications {"count":1,"processorId":"doomed-worker"} +2026-10-16T12:42:10.191Z info: Database connection closed +2026-10-16T12:42:10.200Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:10.224Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:10.225Z info: Database migrations completed +2026-10-16T12:42:10.225Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:10.260Z warn: Recovered stale locks {"count":1} +2026-10-16T12:42:10.309Z info: Fetched and locked pending notifications {"count":1,"processorId":"next-worker"} +2026-10-16T12:42:10.319Z info: Database connection closed +2026-10-16T12:42:10.354Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:12.500Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:12.501Z info: Database migrations completed +2026-10-16T12:42:12.501Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +ts-jest[ts-compiler] (WARN) src/services/notification-retry-queue.test.ts:52:66 - +error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in +type 'RetryQueueOptions'. + +52 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:62:66 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +62 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:75:66 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +75 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:96:9 - error TS2353: Object +literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +96 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:120:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +120 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:139:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +139 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:161:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +161 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:188:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +188 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:214:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +214 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:248:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +248 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:272:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +272 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:288:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +288 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue.test.ts:326:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +326 baseDelayMs: 100, +   ~~~~~~~~~~~ +2026-10-16T12:42:13.517Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"webhook"} +2026-10-16T12:42:13.893Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"webhook"} +2026-10-16T12:42:13.926Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"webhook"} +2026-10-16T12:42:13.929Z info: Database connection closed +2026-10-16T12:42:13.962Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:14.016Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:14.017Z info: Database migrations completed +2026-10-16T12:42:14.017Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:14.023Z info: Database connection closed +2026-10-16T12:42:14.035Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +3817.8322590651774, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:42:17.925Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +2026-10-16T12:42:14.903Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:14.904Z info: Database migrations completed +2026-10-16T12:42:14.906Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:15.087Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:15.109Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:42:15.140Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:42:15.185Z info: Fetched and locked pending notifications {"count":3,"processorId":"worker-1"} +2026-10-16T12:42:15.193Z info: Database connection closed +2026-10-16T12:42:15.226Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:16.575Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:16.577Z info: Database migrations completed +2026-10-16T12:42:16.577Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:16.605Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:16.624Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:42:16.644Z info: Fetched and locked pending notifications {"count":2,"processorId":"worker-1"} +2026-10-16T12:42:16.771Z warn: Recovered stale locks {"count":1} +2026-10-16T12:42:16.783Z info: Database connection closed +2026-10-16T12:42:16.810Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:19.640Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:19.642Z info: Database migrations completed +2026-10-16T12:42:19.643Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:19.676Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:19.708Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T12:42:19.770Z info: Notification marked as completed {"id":1} +2026-10-16T12:42:19.791Z info: Database connection closed +2026-10-16T12:42:19.823Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +PASS src/api/content-type.test.ts (5.549 s) +2026-10-16T12:42:20.603Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:20.627Z info: Database migrations completed +2026-10-16T12:42:20.628Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:20.686Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:20.705Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T12:42:20.744Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3,"nextRetryAt":"2026-10-16T12:43:20.706Z"} +2026-10-16T12:42:20.810Z info: Fetched due retries {"count":1,"processorId":"retry-worker"} +2026-10-16T12:42:20.823Z info: Database connection closed +2026-10-16T12:42:20.843Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:21.621Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:21.622Z info: Database migrations completed +2026-10-16T12:42:21.623Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-scheduled-notification-queue.db"} +2026-10-16T12:42:21.652Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:21.677Z info: Fetched and locked pending notifications {"count":1,"processorId":"worker-1"} +2026-10-16T12:42:21.738Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"discord","failureReason":"permanent outage"} +2026-10-16T12:42:21.740Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":3} +2026-10-16T12:42:21.787Z info: Database connection closed +FAIL src/services/scheduled-notification-queue.test.ts (14.471 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +PASS src/api/notifications-history.test.ts +ts-jest[ts-compiler] (WARN) src/utils/logger.test.ts:191:27 - error TS2345: +Argument of type '(_info: unknown, next: () => void) => void' is not assignable to parameter of type '(...args: +unknown[]) => any'. + Types of parameters 'next' and 'args' are incompatible. + Type 'unknown' is not assignable to type '() => void'. + +191 .mockImplementation((_info: unknown, next: () => void) => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/utils/logger.test.ts:310:27 - error TS2345: Argument of type '(_info: +unknown, next: () => void) => void' is not assignable to parameter of type '(...args: unknown[]) => any'. + Types of parameters 'next' and 'args' are incompatible. + Type 'unknown' is not assignable to type '() => void'. + +310 .mockImplementation((_info: unknown, next: () => void) => { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +PASS src/utils/logger.test.ts +ts-jest[ts-compiler] (WARN) src/services/event-subscriber-backfill.test.ts:51:3 - +error TS1117: An object literal cannot have multiple properties with the same name. + +51 validateRpcResponse: jest.fn().mockReturnValue({ valid: true }), +   ~~~~~~~~~~~~~~~~~~~ +src/services/event-subscriber-backfill.test.ts:83:3 - error TS2741: +Property 'eventBatchSize' is missing in type '{ stellarNetwork: string; stellarNetworkPassphrase: string; +stellarRpcUrl: string; contractAddresses: ContractConfig[]; pollIntervalMs: number; maxReconnectAttempts: number; +reconnectDelayMs: number; eventsApiPort: number; eventsApiCorsOrigin: string; backfill: { ...; } | undefined; }' but +required in type 'Config'. + +83 return { +   ~~~~~~ + + src/types/index.ts:72:3 + 72 eventBatchSize: number; +    ~~~~~~~~~~~~~~ + 'eventBatchSize' is declared here. +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +2026-10-16T12:42:28.024Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:29.177Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:29.184Z info: Database migrations completed +2026-10-16T12:42:29.185Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:29.217Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:29.255Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:42:29.282Z info: Notification marked for retry or failed {"id":1,"newStatus":"PENDING","retryCount":2,"maxRetries":3} +2026-10-16T12:42:29.308Z info: Notification marked as completed {"id":1} +2026-10-16T12:42:29.641Z info: Database connection closed +2026-10-16T12:42:29.656Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +ts-jest[ts-compiler] (WARN) src/services/notification-retry-queue-refactored.test.ts:39:66 +- error TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist +in type 'RetryQueueOptions'. + +39 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:51:66 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +51 const queue = new NotificationRetryQueue(notificationFn, { baseDelayMs: 1000 }); +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:72:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +72 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:97:9 - error TS2353: +Object literal may only specify known properties, and 'baseDelayMs' does not exist in type 'RetryQueueOptions'. + +97 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:117:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +117 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:146:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +146 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:174:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +174 baseDelayMs: 1000, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:205:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +205 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:241:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +241 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:270:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +270 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:287:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +287 baseDelayMs: 100, +   ~~~~~~~~~~~ +src/services/notification-retry-queue-refactored.test.ts:309:9 - error +TS2353: Object literal may only specify known properties, and 'baseDelayMs' does not exist in type +'RetryQueueOptions'. + +309 baseDelayMs: 100, +   ~~~~~~~~~~~ +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 6018.786605702059, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:42:36.357Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +2026-10-16T12:42:30.803Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:30.804Z info: Database migrations completed +2026-10-16T12:42:30.805Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:30.869Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:30.951Z info: Notification marked as completed {"id":1} +2026-10-16T12:42:30.979Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:42:31.059Z info: Notification marked for retry or failed {"id":2,"newStatus":"PENDING","retryCount":1,"maxRetries":3} +2026-10-16T12:42:31.110Z info: Notification marked as completed {"id":2} +2026-10-16T12:42:31.139Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:42:31.211Z info: Notification marked for retry or failed {"id":3,"newStatus":"PENDING","retryCount":1,"maxRetries":2} +2026-10-16T12:42:31.537Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 2"} +2026-10-16T12:42:31.573Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":2,"maxRetries":2} +2026-10-16T12:42:31.688Z warn: Notification moved to dead letter queue {"id":3,"notificationType":"discord","failureReason":"Error 3"} +2026-10-16T12:42:31.689Z info: Notification marked for retry or failed {"id":3,"newStatus":"DEAD_LETTERED","retryCount":3,"maxRetries":2} +2026-10-16T12:42:32.286Z info: Database connection closed +2026-10-16T12:42:32.488Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:33.924Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:33.926Z info: Database migrations completed +2026-10-16T12:42:33.926Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:35.760Z info: Database connection closed +2026-10-16T12:42:35.844Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +PASS src/services/subscription-service.test.ts (5.676 s) +2026-10-16T12:42:36.922Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:36.923Z info: Database migrations completed +2026-10-16T12:42:36.923Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:36.978Z info: Database connection closed +2026-10-16T12:42:36.996Z info: Connected to SQLite database {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:38.089Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:42:38.091Z info: Database migrations completed +2026-10-16T12:42:38.092Z info: Database initialized successfully {"path":"C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\test-data\\test-api-metrics.db"} +2026-10-16T12:42:38.151Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:42:38.192Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:42:38.254Z info: Notification marked as completed {"id":2} +2026-10-16T12:42:38.372Z info: Database connection closed +FAIL src/api/execution-metrics-api.test.ts (12.289 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +2026-10-16T12:42:41.489Z error: Failed to create template {"error":{"message":"uniqueKey: is required and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: uniqueKey: is required and must be a non-empty string; channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK; bodyTemplate: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:85:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.540Z error: Failed to create template {"error":{"message":"channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:126:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:107:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.554Z error: Failed to create template {"error":{"message":"Invalid JSON body","name":"Error","stack":"Error: Invalid JSON body\n at EventEmitter. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:42:16)\n at EventEmitter.emit (node:events:509:28)\n at C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:26:9\n at processTicksAndRejections (node:internal/process/task_queues:85:11)"},"requestId":"req-1"} +2026-10-16T12:42:41.571Z error: Failed to create template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:141:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:137:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.590Z error: Failed to create template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleCreateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:150:5)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:432:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:153:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.625Z error: Failed to list templates {"error":{"message":"channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at handleListTemplates (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:180:13)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:438:11)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:165:33)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} +2026-10-16T12:42:41.668Z info: Listed templates via API {"requestId":"req-1","count":0,"channelType":"EMAIL","activeOnly":false} +2026-10-16T12:42:41.704Z error: Failed to update template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:201:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.708Z error: Failed to update template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:283:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:215:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.715Z error: Failed to update template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleUpdateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:288:5)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:468:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:226:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.724Z error: Failed to render template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:237:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.736Z error: Failed to render template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:348:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:247:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.748Z error: Failed to render template {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleRenderTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:360:7)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:444:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:261:7)"},"requestId":"req-1"} +2026-10-16T12:42:41.774Z error: Failed to get template stats {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:396:9)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:11)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:273:33)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"requestId":"req-1"} +2026-10-16T12:42:41.792Z error: Failed to get template stats {"error":{"message":"res.setHeader is not a function","name":"TypeError","stack":"TypeError: res.setHeader is not a function\n at sendJson (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:53:7)\n at handleGetTemplateStats (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:407:5)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at handleTemplateRoutes (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.ts:450:5)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\api\\template-routes.test.ts:283:7)"},"requestId":"req-1"} +FAIL src/api/template-routes.test.ts + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +PASS src/api/templates-api.test.ts (5.801 s) +PASS src/examples/webhook-consumer-verification-example.test.ts +2026-10-16T12:42:47.207Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"753efd48","correlationId":"79209e50-7410-4001-8ee5-824c1e23bd7b","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":2} +2026-10-16T12:42:47.247Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"758bdbe3","correlationId":"83924f1f-94ea-4c26-aae6-5d75d3628c2f","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":0} +2026-10-16T12:42:47.264Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"42ebda17","correlationId":"9402c4f5-31b8-4ca3-986c-4839264a9ff2","totalRecorded":100,"successRate":0.9,"reset":true,"durationMs":1} +2026-10-16T12:42:47.269Z info: Delivery metrics window reset after read {"requestId":"42ebda17","correlationId":"9402c4f5-31b8-4ca3-986c-4839264a9ff2"} +2026-10-16T12:42:47.288Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"b0a48172","correlationId":"66cd9451-1a10-4de6-9740-cf91f1b0b8da","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":1} +2026-10-16T12:42:47.307Z info: Handling GET /api/notifications/delivery-metrics {"requestId":"81fc96cb","correlationId":"1d8e604b-15fa-4479-973a-5a9a4fc6db56","totalRecorded":100,"successRate":0.9,"reset":false,"durationMs":0} +PASS src/api/delivery-metrics-api.test.ts +2026-10-16T12:42:47.322Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"fe3f6e4a","correlationId":"e811c821-55a5-4963-8e03-b5d83fd87cce","limit":50} +2026-10-16T12:42:47.325Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"fe3f6e4a","correlationId":"e811c821-55a5-4963-8e03-b5d83fd87cce","count":1,"durationMs":5} +2026-10-16T12:42:47.352Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"285100e9","correlationId":"ea0ed6da-0250-4bf6-ba5b-530a9d38d582","limit":10} +2026-10-16T12:42:47.355Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"285100e9","correlationId":"ea0ed6da-0250-4bf6-ba5b-530a9d38d582","count":0,"durationMs":3} +2026-10-16T12:42:47.367Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"7babcd69","correlationId":"035fc4fa-1952-4960-9fe7-a7aa293df005","limit":50,"since":"2024-01-01T00:00:00.000Z"} +2026-10-16T12:42:47.368Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"7babcd69","correlationId":"035fc4fa-1952-4960-9fe7-a7aa293df005","count":0,"durationMs":2} +2026-10-16T12:42:47.402Z info: Handling GET /api/notifications/delivery-metrics/history {"requestId":"0e34f38f","correlationId":"a7f15790-e964-427d-953e-bf3a00e6979c","limit":50} +2026-10-16T12:42:47.403Z info: GET /api/notifications/delivery-metrics/history complete {"requestId":"0e34f38f","correlationId":"a7f15790-e964-427d-953e-bf3a00e6979c","count":0,"durationMs":2} +PASS src/services/subscription-event-emitter.test.ts +PASS src/middleware/body-limit.test.ts +PASS src/services/notification-stats-cache.test.ts +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +2026-10-16T12:42:51.742Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T12:42:51.795Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T12:42:51.856Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T12:42:51.919Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T12:42:51.979Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +2026-10-16T12:42:52.042Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":50,"maxRequestsPerSecond":10,"burstSize":2} +PASS src/__tests__/shutdown-unit.test.ts +2026-10-16T12:42:52.215Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.228Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.245Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.259Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.278Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.291Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.307Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.322Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.339Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.354Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.369Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.386Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.402Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.416Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.430Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.445Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.461Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.476Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.491Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.507Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.522Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.538Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.554Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.571Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.587Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.600Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.617Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.631Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.645Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.661Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.676Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.691Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.707Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.723Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.738Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.754Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.770Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.785Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.801Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.817Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.833Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.853Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.878Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.893Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.909Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.924Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.940Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.955Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.970Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:52.987Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.003Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.019Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.034Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.052Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.068Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.086Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.097Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.112Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.128Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.144Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.159Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.174Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +2026-10-16T12:42:53.190Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/services/rpc-rate-limiter.test.ts +2026-10-16T12:42:53.206Z warn: RPC request throttled {"tokensRemaining":"[REDACTED]","delayMs":10,"maxRequestsPerSecond":10,"burstSize":5} +PASS src/services/retry-scheduler.test.ts +2026-10-16T12:42:53.528Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:42:53.529Z info: No active jobs - proceeding with shutdown +2026-10-16T12:42:53.530Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:42:53.530Z info: Worker manager force shutdown completed +2026-10-16T12:42:53.536Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:42:53.537Z info: No active jobs - proceeding with shutdown +2026-10-16T12:42:53.537Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:42:53.538Z info: Worker manager force shutdown completed +2026-10-16T12:42:53.543Z info: Worker manager force shutdown completed +2026-10-16T12:42:53.547Z info: Worker manager force shutdown completed +2026-10-16T12:42:53.553Z info: Worker manager force shutdown completed +2026-10-16T12:42:53.558Z info: Worker manager force shutdown completed +2026-10-16T12:42:53.562Z info: Worker manager force shutdown completed +PASS src/__tests__/load-test-runner.test.ts +PASS src/api/events-server.health.test.ts +PASS src/services/idempotency-key-service.test.ts (24.979 s) +2026-10-16T12:43:25.239Z info: Worker manager force shutdown completed +2026-10-16T12:43:25.359Z info: Worker manager force shutdown completed +2026-10-16T12:43:25.408Z info: Worker manager force shutdown completed +2026-10-16T12:43:25.461Z info: Worker manager force shutdown completed +FAIL src/__tests__/retry-scheduler.integration.test.ts (26.933 s) + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +2026-10-16T12:43:25.571Z info: Worker manager force shutdown completed +2026-10-16T12:43:27.577Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T12:43:27.591Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T12:43:27.593Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:27.594Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:27.596Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T12:43:27.601Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:27.603Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:27.604Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:27.606Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154608606} +2026-10-16T12:43:27.607Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T12:43:27.610Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:27.610Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:27.611Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154608611} +2026-10-16T12:43:27.611Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T12:43:27.612Z warn: Circuit breaker is OPEN, blocking request {"state":"open","nextAttemptTime":1792154608611} +2026-10-16T12:43:27.710Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:27.711Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:27.712Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154608711} +2026-10-16T12:43:27.713Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T12:43:28.840Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt {"previousState":"open","newState":"half_open"} +2026-10-16T12:43:28.853Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:28.856Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:28.857Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154609856} +2026-10-16T12:43:28.857Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T12:43:29.974Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt {"previousState":"open","newState":"half_open"} +2026-10-16T12:43:29.978Z info: Circuit breaker recorded success {"state":"half_open","successCount":1,"failureCount":3} +2026-10-16T12:43:29.980Z info: Circuit breaker closed after successful recovery {"successCount":0,"successThreshold":2} +2026-10-16T12:43:29.986Z info: Circuit breaker recorded success {"state":"closed","successCount":0,"failureCount":0} +2026-10-16T12:43:29.990Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:29.990Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:29.990Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154610990} +2026-10-16T12:43:29.991Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +PASS src/services/payload-validation.integration.test.ts (5.15 s) +PASS src/services/circuit-breaker.test.ts (5.967 s) +2026-10-16T12:43:31.112Z info: Circuit breaker transitioned to HALF_OPEN for recovery attempt {"previousState":"open","newState":"half_open"} +2026-10-16T12:43:31.113Z info: Circuit breaker recorded success {"state":"half_open","successCount":1,"failureCount":3} +2026-10-16T12:43:31.119Z error: Circuit breaker opened due to repeated failures {"failureCount":4,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154612118} +2026-10-16T12:43:31.121Z warn: Circuit breaker recorded failure {"state":"open","failureCount":4,"successCount":0} +2026-10-16T12:43:31.124Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:31.124Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:31.125Z error: Circuit breaker opened due to repeated failures {"failureCount":3,"failureThreshold":3,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154612125} +2026-10-16T12:43:31.125Z warn: Circuit breaker recorded failure {"state":"open","failureCount":3,"successCount":0} +2026-10-16T12:43:31.126Z info: Circuit breaker manually reset {"previousState":"closed"} +2026-10-16T12:43:31.135Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":1,"successCount":0} +2026-10-16T12:43:31.136Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":2,"successCount":0} +2026-10-16T12:43:31.137Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":3,"successCount":0} +2026-10-16T12:43:31.138Z warn: Circuit breaker recorded failure {"state":"closed","failureCount":4,"successCount":0} +2026-10-16T12:43:31.139Z error: Circuit breaker opened due to repeated failures {"failureCount":5,"failureThreshold":5,"recoveryTimeoutMs":1000,"nextAttemptTime":1792154612138} +2026-10-16T12:43:31.139Z warn: Circuit breaker recorded failure {"state":"open","failureCount":5,"successCount":0} +2026-10-16T12:43:31.142Z info: Circuit breaker recorded success {"state":"closed","successCount":1,"failureCount":0} +2026-10-16T12:43:32.914Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:32.929Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:32.930Z info: Database migrations completed +2026-10-16T12:43:32.930Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:32.934Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:32.939Z info: Database connection closed +2026-10-16T12:43:32.942Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:32.954Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:32.955Z info: Database migrations completed +2026-10-16T12:43:32.955Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:32.988Z info: Database connection closed +2026-10-16T12:43:32.991Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.001Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.002Z info: Database migrations completed +2026-10-16T12:43:33.002Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.005Z info: Database connection closed +2026-10-16T12:43:33.008Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.023Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.023Z info: Database migrations completed +2026-10-16T12:43:33.024Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.025Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.030Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"admin@example.com","action":"UPDATE"} +2026-10-16T12:43:33.032Z info: Notification template updated {"templateId":"tmpl-001","actor":"admin@example.com"} +2026-10-16T12:43:33.038Z info: Database connection closed +2026-10-16T12:43:33.039Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.054Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.055Z info: Database migrations completed +2026-10-16T12:43:33.055Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.056Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.059Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"editor-1","action":"UPDATE"} +2026-10-16T12:43:33.060Z info: Notification template updated {"templateId":"tmpl-001","actor":"editor-1"} +2026-10-16T12:43:33.061Z info: Template audit record created {"auditId":2,"templateId":"tmpl-001","actor":"editor-2","action":"UPDATE"} +2026-10-16T12:43:33.062Z info: Notification template updated {"templateId":"tmpl-001","actor":"editor-2"} +2026-10-16T12:43:33.064Z info: Database connection closed +2026-10-16T12:43:33.067Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.079Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.080Z info: Database migrations completed +2026-10-16T12:43:33.080Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.084Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.088Z info: Database connection closed +2026-10-16T12:43:33.091Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.101Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.101Z info: Database migrations completed +2026-10-16T12:43:33.102Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.103Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.107Z info: Database connection closed +2026-10-16T12:43:33.109Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.121Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.121Z info: Database migrations completed +2026-10-16T12:43:33.122Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.123Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.126Z info: Database connection closed +2026-10-16T12:43:33.129Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.156Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.157Z info: Database migrations completed +2026-10-16T12:43:33.158Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.160Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.162Z info: [TemplateCache] Template cached {"templateId":"tmpl-001","ttl":60} +2026-10-16T12:43:33.178Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"cache-admin","action":"UPDATE"} +2026-10-16T12:43:33.181Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-001","deleted":1} +2026-10-16T12:43:33.193Z info: Notification template updated {"templateId":"tmpl-001","actor":"cache-admin"} +2026-10-16T12:43:33.204Z info: Database connection closed +2026-10-16T12:43:33.213Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.227Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.228Z info: Database migrations completed +2026-10-16T12:43:33.228Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.230Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.237Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} +2026-10-16T12:43:33.239Z info: Notification template updated {"templateId":"tmpl-001","actor":"auditor"} +2026-10-16T12:43:33.241Z error: Database run error {"sql":"UPDATE notification_template_audit_log SET actor = ? WHERE id = ?","params":["tampered",1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:43:33.247Z info: Database connection closed +2026-10-16T12:43:33.257Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.276Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.277Z info: Database migrations completed +2026-10-16T12:43:33.277Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.288Z info: Notification template created {"templateId":"tmpl-001"} +2026-10-16T12:43:33.295Z info: Template audit record created {"auditId":1,"templateId":"tmpl-001","actor":"auditor","action":"UPDATE"} +2026-10-16T12:43:33.296Z info: Notification template updated {"templateId":"tmpl-001","actor":"auditor"} +2026-10-16T12:43:33.301Z error: Database run error {"sql":"DELETE FROM notification_template_audit_log WHERE id = ?","params":[1],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:43:33.303Z info: Database connection closed +2026-10-16T12:43:33.306Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.324Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.325Z info: Database migrations completed +2026-10-16T12:43:33.325Z info: Database initialized successfully {"path":":memory:"} +PASS src/services/notification-template-repository.test.ts +2026-10-16T12:43:33.343Z info: Database connection closed +2026-10-16T12:43:33.346Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:33.370Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:33.371Z info: Database migrations completed +2026-10-16T12:43:33.372Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:33.375Z info: Database connection closed +PASS src/utils/logger-structured.test.ts +PASS src/utils/circuit-breaker.test.ts (5.397 s) +2026-10-16T12:43:36.345Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:36.357Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:36.358Z info: Database migrations completed +2026-10-16T12:43:36.358Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:36.387Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.395Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.398Z info: Notification marked as completed {"id":2} +2026-10-16T12:43:36.405Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.408Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.409Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.411Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.412Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.413Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.415Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.420Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.421Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.422Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.429Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.430Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.431Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.437Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.439Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} +2026-10-16T12:43:36.442Z info: Notification marked as completed {"id":13} +2026-10-16T12:43:36.443Z info: Notification marked as completed {"id":14} +2026-10-16T12:43:36.445Z info: Notification marked as completed {"id":15} +PASS src/services/scheduled-notification-repository-cache.test.ts +2026-10-16T12:43:36.458Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} +PASS src/services/search-suggestion.test.ts +2026-10-16T12:43:37.442Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:43:37.456Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:37.457Z info: Database migrations completed +2026-10-16T12:43:37.458Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:43:37.521Z info: Database connection closed +PASS src/tests/notification-schema-compatibility.test.ts +PASS src/config-logging.test.ts +2026-10-16T12:43:40.254Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:40.255Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:40.255Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:40.256Z info: Worker manager force shutdown completed +ts-jest[ts-compiler] (WARN) src/tests/events-server.test.ts:9:4 - error +TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value. + +9 ): Promise<{ status: number; headers: http.IncomingHttpHeaders }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:10:14 - error TS2552: Cannot find name +'headers'. Did you mean 'Headers'? + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~~ + + node_modules/@types/node/web-globals/fetch.d.ts:47:9 + 47 var Headers: typeof globalThis extends { onmessage: any; Headers: infer T } ? T : typeof +undici.Headers; +    ~~~~~~~ + 'Headers' is declared here. +src/tests/events-server.test.ts:10:24 - error TS2693: 'Record' only +refers to a type, but is being used as a value here. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:10:48 - error TS2304: Cannot find name +'method'. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:10:57 - error TS2693: 'string' only +refers to a type, but is being used as a value here. + +10 options: { headers?: Record; method?: string } = {} +   ~~~~~~ +src/tests/events-server.test.ts:11:4 - error TS2365: Operator '<' cannot +be applied to types 'PromiseConstructor' and '{ status: any; headers: any; body: any; }'. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:11:4 - error TS2365: Operator '>' cannot +be applied to types 'boolean' and '{ return: Promise; }'. + + 11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 12 return new Promise((resolve, reject) => { +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 26 }); +  ~~~~~ + 27 } +  ~ +src/tests/events-server.test.ts:11:22 - error TS2693: 'number' only +refers to a type, but is being used as a value here. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~ +src/tests/events-server.test.ts:11:44 - error TS2339: Property +'IncomingHttpHeaders' does not exist on type 'typeof import("http")'. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~~~~~~~~~~~~~~~~~ +src/tests/events-server.test.ts:11:71 - error TS2693: 'any' only refers +to a type, but is being used as a value here. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~~~ +src/tests/events-server.test.ts:150:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +150 const { status, body } = await makeRequest('/api/events'); +   ~~~~ +src/tests/events-server.test.ts:157:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +157 const { status, body } = await makeRequest('/api/events?limit=50'); +   ~~~~ +src/tests/events-server.test.ts:163:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +163 const { status, body } = await makeRequest('/api/events?limit=200'); +   ~~~~ +src/tests/events-server.test.ts:169:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +169 const { status, body } = await makeRequest('/api/events?limit=-10'); +   ~~~~ +src/tests/events-server.test.ts:175:21 - error TS2339: Property 'body' +does not exist on type '{ status: number; headers: IncomingHttpHeaders; }'. + +175 const { status, body } = await makeRequest('/api/events?limit=invalid'); +   ~~~~ +src/tests/events-server.test.ts:10:22 - error TS1109: Expression expected. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:10:55 - error TS1109: Expression expected. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:10:66 - error TS2809: Declaration or +statement expected. This '=' follows a block of statements, so if you intended to write a destructuring assignment, +you might need to wrap the whole assignment in parentheses. + +10 options: { headers?: Record; method?: string } = {} +   ~ +src/tests/events-server.test.ts:11:1 - error TS1128: Declaration or +statement expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +  ~ +src/tests/events-server.test.ts:11:2 - error TS1128: Declaration or +statement expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:11:28 - error TS1005: ',' expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:11:63 - error TS1005: ',' expected. + +11 ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { +   ~ +src/tests/events-server.test.ts:12:10 - error TS1005: ':' expected. + +12 return new Promise((resolve, reject) => { +   ~~~ +src/tests/events-server.test.ts:23:5 - error TS1128: Declaration or +statement expected. + +23 ); +   ~ +src/tests/events-server.test.ts:26:5 - error TS1005: ',' expected. + +26 }); +   ~ +src/tests/events-server.test.ts:180:1 - error TS1005: '}' expected. + +180 +   + + src/tests/events-server.test.ts:77:46 + 77 describe('GET /api/events pagination', () => { +    ~ + The parser expected to find a '}' to match the '{' token here. +FAIL src/tests/events-server.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\tests\events-server.test.ts:11 + headers ? : Record; + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +2026-10-16T12:43:40.559Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:40.560Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:40.560Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:40.560Z info: Worker manager force shutdown completed +2026-10-16T12:43:41.320Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:41.321Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:41.321Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:41.322Z info: Worker manager force shutdown completed +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +PASS src/__tests__/scheduled-notification-lifecycle.e2e.test.ts +2026-10-16T12:43:41.708Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:41.709Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:41.709Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:41.710Z info: Worker manager force shutdown completed +2026-10-16T12:43:42.609Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:42.611Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:42.612Z warn: Job rejected - worker is shutting down {"jobId":"job-1"} +2026-10-16T12:43:42.612Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:42.622Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:42.622Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:42.623Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:42.624Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:42.624Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:42.625Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:42.626Z info: Initiating graceful shutdown {"activeJobs":2} +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +2026-10-16T12:43:43.639Z info: All jobs completed during graceful shutdown +2026-10-16T12:43:43.639Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:43.642Z info: Initiating graceful shutdown {"activeJobs":1} +2026-10-16T12:43:43.644Z info: Initiating graceful shutdown {"activeJobs":1} +2026-10-16T12:43:43.645Z warn: Job rejected - worker is shutting down {"jobId":"job-2"} +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +2026-10-16T12:43:44.170Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.172Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be an integer, received 'abc'"} +2026-10-16T12:43:44.176Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.176Z warn: Archive query rejected {"requestId":"req-1","error":"limit: must be <= 100"} +2026-10-16T12:43:44.182Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.183Z warn: Archive query rejected {"requestId":"req-1","error":"offset: must be >= 0"} +2026-10-16T12:43:44.186Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.187Z warn: Archive query rejected {"requestId":"req-1","error":"status: must be one of: PENDING, PROCESSING, COMPLETED, FAILED, DEAD_LETTERED, CANCELLED, EXPIRED"} +2026-10-16T12:43:44.189Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.190Z warn: Archive query rejected {"requestId":"req-1","error":"startDate: must be a valid date, received 'not-a-date'"} +2026-10-16T12:43:44.192Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.195Z info: Handling GET /api/archive {"requestId":"req-1"} +2026-10-16T12:43:44.200Z info: Handling GET /api/archive/:id {"requestId":"req-1","id":42} +2026-10-16T12:43:44.206Z info: Handling POST /api/archive/run {"requestId":"req-1"} +PASS src/services/worker-manager.test.ts +2026-10-16T12:43:44.645Z info: All jobs completed during graceful shutdown +2026-10-16T12:43:44.645Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:44.647Z info: Initiating graceful shutdown {"activeJobs":0} +2026-10-16T12:43:44.647Z info: No active jobs - proceeding with shutdown +2026-10-16T12:43:44.648Z warn: Graceful shutdown already initiated +2026-10-16T12:43:44.648Z info: All jobs have completed - shutdown can proceed +2026-10-16T12:43:44.651Z info: Worker manager force shutdown completed +2026-10-16T12:43:44.653Z info: Worker manager force shutdown completed +2026-10-16T12:43:45.437Z info: Connected to SQLite database {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-3292-1792154625429.db"} +PASS src/utils/validation.test.ts +ts-jest[ts-compiler] (WARN) src/services/event-normalization.test.ts:101:35 - +error TS2353: Object literal may only specify known properties, and 'payload_version' does not exist +in type 'ScMapEntry[]'. + +101 value: xdr.ScVal.scvMap({ payload_version: 1, notification_id: 'abc' }), +   ~~~~~~~~~~~~~~~ +PASS src/services/event-normalization.test.ts +2026-10-16T12:43:48.571Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:48.572Z info: Database migrations completed +2026-10-16T12:43:48.573Z info: Database initialized successfully {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-idx003-3292-1792154625429.db"} +2026-10-16T12:43:48.585Z error: Database run error {"sql":"INSERT INTO scheduled_notifications\n (payload, notification_type, target_recipient, execute_at, status,\n processing_completed_at, created_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)","params":["{}","Webhook","user-2","2026-01-01T00:02:00.000Z","FAILED","2026-01-01T00:02:00.000Z","2026-01-01T00:02:00.000Z"],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:43:48.598Z info: Database connection closed +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +2026-10-16T12:43:53.275Z info: Connected to SQLite database {"path":"./data/test-integrity.db"} +2026-10-16T12:43:54.473Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:43:54.476Z info: Database migrations completed +2026-10-16T12:43:54.476Z info: Database initialized successfully {"path":"./data/test-integrity.db"} +2026-10-16T12:43:54.547Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T12:43:54.576Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:43:54.675Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T12:43:54.697Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:43:54.826Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T12:43:54.909Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +FAIL src/tests/payload-integrity.test.ts (7.21 s) + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +2026-10-16T12:43:55.004Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"https://discord.com/webhook/test","version":1} +2026-10-16T12:43:55.026Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:43:55.037Z info: Database connection closed +PASS src/schema/compatibility-check.test.ts (7.422 s) +2026-10-16T12:43:58.662Z info: Template created successfully {"templateId":1,"uniqueKey":"welcome-email"} +2026-10-16T12:43:58.730Z error: Failed to create template {"error":{"message":"name: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:44:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T12:43:58.827Z error: Failed to create template {"error":{"message":"name: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:52:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":" ","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T12:43:58.844Z error: Failed to create template {"error":{"message":"channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK","name":"ValidationError","stack":"ValidationError: channelType: must be one of: EMAIL, SMS, DISCORD, PUSH, WEBHOOK\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:59:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"CARRIER_PIGEON","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T12:43:58.848Z error: Failed to create template {"error":{"message":"description: must be a string","name":"ValidationError","stack":"ValidationError: description: must be a string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:67:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!","description":12345}} +2026-10-16T12:43:58.853Z error: Failed to create template {"error":{"message":"name: must not exceed 255 characters","name":"ValidationError","stack":"ValidationError: name: must not exceed 255 characters\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:74:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!"}} +2026-10-16T12:43:58.858Z error: Failed to create template {"error":{"message":"variables: must be an array of strings","name":"ValidationError","stack":"ValidationError: variables: must be an array of strings\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:81:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!","variables":[1,2,3]}} +2026-10-16T12:43:58.862Z error: Failed to create template {"error":{"message":"defaultValues: must be an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.createTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:91:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:88:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"input":{"uniqueKey":"welcome-email","name":"Welcome Email","channelType":"EMAIL","subjectTemplate":"Welcome {{name}}","bodyTemplate":"Hello {{name}}, welcome aboard!","defaultValues":"nope"}} +2026-10-16T12:43:58.870Z info: Template updated successfully {"id":1} +2026-10-16T12:43:58.905Z error: Failed to update template {"error":{"message":"name: is required and must be a non-empty string","name":"ValidationError","stack":"ValidationError: name: is required and must be a non-empty string\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:128:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} +2026-10-16T12:43:58.909Z info: Template updated successfully {"id":1} +2026-10-16T12:43:58.920Z error: Failed to update template {"error":{"message":"defaultValues: must be an object","name":"ValidationError","stack":"ValidationError: defaultValues: must be an object\n at InputValidator.throwIfInvalid (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\utils\\validation.ts:53:13)\n at validateTemplateFields (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:74:5)\n at TemplateService.updateTemplate (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.ts:166:7)\n at Object. (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\src\\services\\template-service.test.ts:140:34)\n at Promise.finally.completed (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1834:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1774:10)\n at _callCircusTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1167:40)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at _runTestInContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1103:3)\n at runTestWithContext (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:996:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1008:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at _runTestsForDescribeBlockOnce (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1003:11)\n at _runTestsForDescribeBlock (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:824:5)\n at run (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:789:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\jestAdapterInit.js:2300:21)\n at jestAdapter (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:247:16)\n at runTest (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:315:7)\n at Object.worker (C:\\Users\\NEW OWNER\\Documents\\drips\\core foundry 941\\Notify-Chain\\listener\\node_modules\\jest-runner\\build\\testWorker.js:479:12)"},"id":1} +PASS src/services/template-service.test.ts +PASS src/utils/request-id.test.ts +PASS src/__tests__/api-contract.test.ts +2026-10-16T12:44:01.736Z info: Handling GET /api/events {"requestId":"e7805459","correlationId":"ba4540e2-fc23-49eb-b169-7baa9565a085","limit":20} +2026-10-16T12:44:01.739Z info: GET /api/events complete {"requestId":"e7805459","correlationId":"ba4540e2-fc23-49eb-b169-7baa9565a085","returned":0,"durationMs":10} +2026-10-16T12:44:01.775Z warn: Unhandled request {"requestId":"40716a2e","correlationId":"ed57c217-6bcd-400f-bb17-dba1a8bf199a","method":"GET","url":"/api/definitely-not-a-route"} +2026-10-16T12:44:01.779Z info: Request completed {"method":"GET","url":"/api/definitely-not-a-route","statusCode":404,"durationMs":4,"requestId":"40716a2e"} +2026-10-16T12:44:01.796Z info: Notification analytics aggregator initialized with defaults +2026-10-16T12:44:01.808Z info: Handling GET /api/analytics {"requestId":"9029f534","correlationId":"a60d18b2-d6e8-416a-9902-3745dd9b6b4e","totalRecorded":0,"reset":false,"durationMs":12} +2026-10-16T12:44:01.823Z info: Handling GET /api/events {"requestId":"e904fbb6","correlationId":"920f81bd-ab92-4107-ac4f-8c4c2f2b80ee","limit":20} +2026-10-16T12:44:01.824Z info: GET /api/events complete {"requestId":"e904fbb6","correlationId":"920f81bd-ab92-4107-ac4f-8c4c2f2b80ee","returned":0,"durationMs":2} +2026-10-16T12:44:03.747Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.757Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.759Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.763Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.771Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.806Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.809Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.814Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +2026-10-16T12:44:03.823Z info: Scheduling new notification {"type":"webhook","executeAt":{},"recipient":"https://hooks.example.com/notify","version":1} +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +PASS src/services/notification-search-service.test.ts +2026-10-16T12:44:05.964Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T12:44:05.976Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T12:44:05.980Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T12:44:05.991Z info: Notification search complete {"total":0,"returned":0,"limit":20,"offset":0} +2026-10-16T12:44:05.995Z info: Notification search complete {"total":1,"returned":1,"limit":20,"offset":0} +2026-10-16T12:44:07.648Z info: Connected to SQLite database {"path":"./data/test-scheduler-concurrency.db"} +2026-10-16T12:44:08.478Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:08.482Z info: Database migrations completed +2026-10-16T12:44:08.485Z info: Database initialized successfully {"path":"./data/test-scheduler-concurrency.db"} +2026-10-16T12:44:08.492Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} +2026-10-16T12:44:08.553Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} +2026-10-16T12:44:08.554Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} +2026-10-16T12:44:08.579Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} +2026-10-16T12:44:08.594Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} +2026-10-16T12:44:08.630Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} +2026-10-16T12:44:08.636Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} +2026-10-16T12:44:08.676Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} +2026-10-16T12:44:10.041Z info: Fetched and locked pending notifications {"requestId":"2ed6866b","count":4,"processorId":"test-uuid-0001"} +2026-10-16T12:44:10.045Z info: Processing batch of scheduled notifications {"requestId":"2ed6866b","count":4,"processorId":"test-uuid-0001"} +2026-10-16T12:44:10.050Z info: Processing scheduled notification {"requestId":"2ed6866b","id":1,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:10.211Z info: Notification marked as completed {"requestId":"2ed6866b","id":1} +2026-10-16T12:44:10.256Z info: Notification delivered successfully {"requestId":"2ed6866b","id":1,"type":"discord","durationMs":98} +2026-10-16T12:44:10.257Z info: Processing scheduled notification {"requestId":"2ed6866b","id":2,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:10.379Z info: Notification marked as completed {"requestId":"2ed6866b","id":2} +2026-10-16T12:44:10.445Z info: Notification delivered successfully {"requestId":"2ed6866b","id":2,"type":"discord","durationMs":86} +2026-10-16T12:44:10.453Z info: Processing scheduled notification {"requestId":"2ed6866b","id":3,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:10.638Z info: Notification marked as completed {"requestId":"2ed6866b","id":3} +2026-10-16T12:44:10.668Z info: Notification delivered successfully {"requestId":"2ed6866b","id":3,"type":"discord","durationMs":91} +2026-10-16T12:44:10.673Z info: Processing scheduled notification {"requestId":"2ed6866b","id":4,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:10.786Z info: Notification marked as completed {"requestId":"2ed6866b","id":4} +2026-10-16T12:44:10.818Z info: Notification delivered successfully {"requestId":"2ed6866b","id":4,"type":"discord","durationMs":91} +2026-10-16T12:44:10.820Z info: Scheduler batch complete {"requestId":"2ed6866b","processorId":"test-uuid-0001","count":4,"durationMs":824} +2026-10-16T12:44:10.829Z info: Worker manager force shutdown completed +2026-10-16T12:44:10.883Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} +2026-10-16T12:44:10.903Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} +2026-10-16T12:44:10.904Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} +2026-10-16T12:44:10.923Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} +2026-10-16T12:44:10.924Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-2","version":1} +2026-10-16T12:44:10.938Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} +2026-10-16T12:44:10.939Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-3","version":1} +2026-10-16T12:44:10.958Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} +2026-10-16T12:44:10.959Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-4","version":1} +2026-10-16T12:44:10.973Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} +2026-10-16T12:44:10.974Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-5","version":1} +2026-10-16T12:44:10.993Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} +2026-10-16T12:44:12.325Z info: Fetched and locked pending notifications {"requestId":"b5e64976","count":6,"processorId":"test-uuid-0002"} +2026-10-16T12:44:12.327Z info: Processing batch of scheduled notifications {"requestId":"b5e64976","count":6,"processorId":"test-uuid-0002"} +2026-10-16T12:44:12.330Z info: Processing scheduled notification {"requestId":"b5e64976","id":5,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:12.330Z info: Processing scheduled notification {"requestId":"b5e64976","id":6,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:12.331Z info: Processing scheduled notification {"requestId":"b5e64976","id":7,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:12.458Z info: Notification marked as completed {"requestId":"b5e64976","id":5} +2026-10-16T12:44:12.459Z info: Notification marked as completed {"requestId":"b5e64976","id":6} +2026-10-16T12:44:12.475Z info: Notification marked as completed {"requestId":"b5e64976","id":7} +2026-10-16T12:44:12.506Z info: Notification delivered successfully {"requestId":"b5e64976","id":5,"type":"discord","durationMs":97} +2026-10-16T12:44:12.507Z info: Processing scheduled notification {"requestId":"b5e64976","id":8,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:12.508Z info: Notification delivered successfully {"requestId":"b5e64976","id":6,"type":"discord","durationMs":96} +2026-10-16T12:44:12.510Z info: Processing scheduled notification {"requestId":"b5e64976","id":9,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:12.523Z info: Notification delivered successfully {"requestId":"b5e64976","id":7,"type":"discord","durationMs":96} +2026-10-16T12:44:12.525Z info: Processing scheduled notification {"requestId":"b5e64976","id":10,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:12.624Z info: Notification marked as completed {"requestId":"b5e64976","id":8} +2026-10-16T12:44:12.625Z info: Notification marked as completed {"requestId":"b5e64976","id":9} +2026-10-16T12:44:12.669Z info: Notification delivered successfully {"requestId":"b5e64976","id":8,"type":"discord","durationMs":88} +2026-10-16T12:44:12.670Z info: Notification marked as completed {"requestId":"b5e64976","id":10} +2026-10-16T12:44:12.671Z info: Notification delivered successfully {"requestId":"b5e64976","id":9,"type":"discord","durationMs":85} +2026-10-16T12:44:12.690Z info: Notification delivered successfully {"requestId":"b5e64976","id":10,"type":"discord","durationMs":101} +2026-10-16T12:44:12.691Z info: Scheduler batch complete {"requestId":"b5e64976","processorId":"test-uuid-0002","count":6,"durationMs":388} +2026-10-16T12:44:12.694Z info: Worker manager force shutdown completed +2026-10-16T12:44:12.728Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-0","version":1} +2026-10-16T12:44:12.754Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} +2026-10-16T12:44:12.754Z info: Scheduling new notification {"type":"discord","executeAt":{},"recipient":"test-webhook-1","version":1} +2026-10-16T12:44:12.772Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} +2026-10-16T12:44:14.502Z info: Fetched and locked pending notifications {"requestId":"fb6d2310","count":2,"processorId":"test-uuid-0003"} +2026-10-16T12:44:14.503Z info: Processing batch of scheduled notifications {"requestId":"fb6d2310","count":2,"processorId":"test-uuid-0003"} +2026-10-16T12:44:14.505Z info: Processing scheduled notification {"requestId":"fb6d2310","id":11,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:14.505Z info: Processing scheduled notification {"requestId":"fb6d2310","id":12,"type":"discord","executeAt":{},"attempt":1} +2026-10-16T12:44:15.284Z info: Notification marked as completed {"requestId":"fb6d2310","id":11} +2026-10-16T12:44:15.286Z info: Notification marked as completed {"requestId":"fb6d2310","id":12} +2026-10-16T12:44:15.953Z info: Notification delivered successfully {"requestId":"fb6d2310","id":11,"type":"discord","durationMs":94} +2026-10-16T12:44:15.954Z info: Notification delivered successfully {"requestId":"fb6d2310","id":12,"type":"discord","durationMs":99} +2026-10-16T12:44:15.954Z info: Scheduler batch complete {"requestId":"fb6d2310","processorId":"test-uuid-0003","count":2,"durationMs":1873} +2026-10-16T12:44:15.958Z info: Worker manager force shutdown completed +2026-10-16T12:44:16.019Z info: Database connection closed +PASS src/__tests__/scheduler-concurrency.test.ts (10.065 s) +2026-10-16T12:44:22.875Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:22.939Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:22.940Z info: Database migrations completed +2026-10-16T12:44:22.941Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:22.952Z info: Database connection closed +2026-10-16T12:44:22.959Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:22.973Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:22.974Z info: Database migrations completed +2026-10-16T12:44:22.974Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:22.977Z info: Database connection closed +2026-10-16T12:44:22.991Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:23.012Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:23.014Z info: Database migrations completed +2026-10-16T12:44:23.021Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:23.027Z info: Event processed and recorded {"eventId":"duplicate-test-event","contractAddress":"CBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB","fingerprint":"4d9a04f2a8a049fbfedbc9fa7e8658adb0a026ae65685d662a4dfeb667cbb3c2","ledgerNumber":12345,"notificationSent":true} +2026-10-16T12:44:23.031Z info: Database connection closed +2026-10-16T12:44:23.035Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:23.047Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:23.048Z info: Database migrations completed +2026-10-16T12:44:23.048Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:23.054Z info: Event processed and recorded {"eventId":"skipped-test-event","contractAddress":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","fingerprint":"e955841f5a10749b353cc37342a68ce8b459de1809d48965790a30685a67dc06","ledgerNumber":12345,"notificationSent":false} +2026-10-16T12:44:23.056Z info: Database connection closed +2026-10-16T12:44:23.059Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:23.073Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:23.074Z info: Database migrations completed +2026-10-16T12:44:23.075Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:23.077Z info: Database connection closed +2026-10-16T12:44:23.080Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:23.094Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:23.095Z info: Database migrations completed +2026-10-16T12:44:23.096Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:23.098Z info: Database connection closed +2026-10-16T12:44:23.109Z info: Connected to SQLite database {"path":":memory:"} +PASS src/scripts/replay-event.test.ts (20.801 s) +2026-10-16T12:44:23.127Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:23.128Z info: Database migrations completed +2026-10-16T12:44:23.128Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:23.130Z info: Database connection closed +2026-10-16T12:44:28.289Z info: Handling GET /api/events {"requestId":"9ffa10cb","correlationId":"7c1143d6-5f3e-4adb-9489-1e249f522741","limit":20} +2026-10-16T12:44:28.293Z info: GET /api/events complete {"requestId":"9ffa10cb","correlationId":"7c1143d6-5f3e-4adb-9489-1e249f522741","returned":0,"durationMs":10} +2026-10-16T12:44:28.310Z info: Handling GET /api/events {"requestId":"ab706e11","correlationId":"3b03421e-29e8-4e1f-a263-2d862b43d838","limit":20} +2026-10-16T12:44:28.311Z info: GET /api/events complete {"requestId":"ab706e11","correlationId":"3b03421e-29e8-4e1f-a263-2d862b43d838","returned":0,"durationMs":2} +2026-10-16T12:44:28.321Z info: Handling GET /api/events {"requestId":"162bb83f","correlationId":"f7d96870-74fc-4205-9010-1fc962647c13","limit":20} +2026-10-16T12:44:28.322Z info: GET /api/events complete {"requestId":"162bb83f","correlationId":"f7d96870-74fc-4205-9010-1fc962647c13","returned":0,"durationMs":2} +2026-10-16T12:44:28.325Z info: Handling GET /api/events {"requestId":"9ce6eeb0","correlationId":"ca44a42e-ab96-4d88-940d-71f21aa42fcd","limit":20} +2026-10-16T12:44:28.326Z info: GET /api/events complete {"requestId":"9ce6eeb0","correlationId":"ca44a42e-ab96-4d88-940d-71f21aa42fcd","returned":0,"durationMs":1} +2026-10-16T12:44:28.331Z info: Handling GET /api/events {"requestId":"894a4fe6","correlationId":"417fcfb7-bb4e-49c9-992b-10f54e4a49c2","limit":20} +2026-10-16T12:44:28.333Z info: GET /api/events complete {"requestId":"894a4fe6","correlationId":"417fcfb7-bb4e-49c9-992b-10f54e4a49c2","returned":0,"durationMs":2} +FAIL src/tests/api-versioning.test.ts +2026-10-16T12:44:28.393Z info: Handling GET /api/events {"requestId":"d14b798c","correlationId":"5203c7ce-b4f0-4db0-a00f-2974c1d5cbd2","limit":20} +2026-10-16T12:44:28.394Z info: GET /api/events complete {"requestId":"d14b798c","correlationId":"5203c7ce-b4f0-4db0-a00f-2974c1d5cbd2","returned":0,"durationMs":1} +2026-10-16T12:44:28.396Z info: Handling GET /api/events {"requestId":"51b36500","correlationId":"69a3200e-9532-4fc5-a259-616d7bfb5b07","limit":20} +2026-10-16T12:44:28.396Z info: GET /api/events complete {"requestId":"51b36500","correlationId":"69a3200e-9532-4fc5-a259-616d7bfb5b07","returned":0,"durationMs":1} +2026-10-16T12:44:28.407Z info: Handling GET /api/events {"requestId":"ce6f39a1","correlationId":"6e9e3a76-a410-44f7-9d5a-69fb70f69e0e","limit":20} +2026-10-16T12:44:28.408Z info: GET /api/events complete {"requestId":"ce6f39a1","correlationId":"6e9e3a76-a410-44f7-9d5a-69fb70f69e0e","returned":0,"durationMs":1} +2026-10-16T12:44:28.413Z info: Handling GET /api/events {"requestId":"db4c8ad4","correlationId":"9fb5841b-0440-4b14-b8f2-662876557c8b","limit":5} +2026-10-16T12:44:28.414Z info: GET /api/events complete {"requestId":"db4c8ad4","correlationId":"9fb5841b-0440-4b14-b8f2-662876557c8b","returned":0,"durationMs":1} +2026-10-16T12:44:28.426Z warn: Unhandled request {"requestId":"a64fc9cb","correlationId":"3ef1e2cc-3637-4671-bc5e-12abbedda2ed","method":"GET","url":"/api/v1/unknown-route"} +2026-10-16T12:44:28.428Z info: Request completed {"method":"GET","url":"/api/v1/unknown-route","statusCode":404,"durationMs":3,"requestId":"a64fc9cb"} + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +2026-10-16T12:44:28.434Z warn: Unhandled request {"requestId":"f6f5dfdf","correlationId":"55d6b733-9533-465b-94a2-73c306e94255","method":"GET","url":"/api/v1/no-such-route"} +2026-10-16T12:44:28.435Z info: Request completed {"method":"GET","url":"/api/v1/no-such-route","statusCode":404,"durationMs":3,"requestId":"f6f5dfdf"} +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +PASS src/services/event-store-consistency.test.ts +2026-10-16T12:44:29.749Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:29.763Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:29.763Z info: Database migrations completed +2026-10-16T12:44:29.764Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:29.814Z info: Database connection closed +2026-10-16T12:44:31.710Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +PASS src/config-schema.test.ts (13.324 s) +2026-10-16T12:44:34.792Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:34.792Z info: Database migrations completed +2026-10-16T12:44:34.793Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:34.846Z error: Database run error {"sql":"INSERT INTO t (v) VALUES ('after')","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T12:44:34.848Z warn: Database connection failure detected; attempting recovery {"path":"./data/test-db-recovery.db","attempt":1,"maxReconnectAttempts":3,"delayMs":10,"error":"SQLITE_MISUSE: Database is closed"} +2026-10-16T12:44:34.892Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:34.894Z warn: Database connection recovered {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:34.954Z info: Database connection closed +2026-10-16T12:44:34.970Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:35.881Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:35.881Z info: Database migrations completed +2026-10-16T12:44:35.886Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:35.932Z error: Failed to connect to database {"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} +2026-10-16T12:44:35.938Z warn: Database connection failure detected; attempting recovery {"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"maxReconnectAttempts":2,"delayMs":1,"error":"SQLITE_CANTOPEN: unable to open database file"} +2026-10-16T12:44:35.956Z error: Failed to connect to database {"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} +2026-10-16T12:44:35.957Z error: Database reconnect attempt failed {"path":"./data/no-such-dir-xyz/db.sqlite","attempt":1,"error":"SQLITE_CANTOPEN: unable to open database file"} +2026-10-16T12:44:35.962Z error: Failed to connect to database {"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} +2026-10-16T12:44:35.963Z warn: Database connection failure detected; attempting recovery {"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"maxReconnectAttempts":2,"delayMs":2,"error":"SQLITE_CANTOPEN: unable to open database file"} +2026-10-16T12:44:35.991Z error: Failed to connect to database {"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} +2026-10-16T12:44:35.991Z error: Database reconnect attempt failed {"path":"./data/no-such-dir-xyz/db.sqlite","attempt":2,"error":"SQLITE_CANTOPEN: unable to open database file"} +2026-10-16T12:44:36.003Z error: Failed to connect to database {"error":"{\"errno\":14,\"code\":\"SQLITE_CANTOPEN\"}","path":"./data/no-such-dir-xyz/db.sqlite"} +2026-10-16T12:44:36.031Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:36.815Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:36.819Z info: Database migrations completed +2026-10-16T12:44:36.820Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:36.859Z error: Database run error {"sql":"INSERT INTO t (v) VALUES ('dup')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:44:36.864Z info: Database connection closed +2026-10-16T12:44:36.878Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +PASS src/utils/payload-size-validator.test.ts +2026-10-16T12:44:38.421Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:38.422Z info: Database migrations completed +2026-10-16T12:44:38.422Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:38.456Z error: Database run error {"sql":"INSERT INTO t (v) VALUES ('kept')","params":[],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:44:38.459Z error: Transaction rolled back {"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} +2026-10-16T12:44:38.462Z info: Database connection closed +2026-10-16T12:44:38.469Z info: Connected to SQLite database {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:38.571Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.576Z info: [TemplateCache] Template cached {"templateId":"tmpl-1","ttl":60} +2026-10-16T12:44:38.580Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.589Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.591Z info: [TemplateCache] Template cached {"templateId":"tmpl-2","ttl":60} +2026-10-16T12:44:38.591Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.606Z info: [TemplateCache] Template cached {"templateId":"tmpl-3","ttl":60} +2026-10-16T12:44:38.607Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.610Z info: [TemplateCache] Template cached {"templateId":"tmpl-4","ttl":60} +2026-10-16T12:44:38.611Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.619Z info: [TemplateCache] Template cached {"templateId":"tmpl-5","ttl":60} +2026-10-16T12:44:38.620Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.623Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.627Z info: [TemplateCache] Template cached {"templateId":"tmpl-6","ttl":60} +2026-10-16T12:44:38.628Z info: [TemplateCache] Template invalidated {"templateId":"tmpl-6","deleted":1} +2026-10-16T12:44:38.629Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.631Z info: [TemplateCache] Template cached {"templateId":"tmpl-7","ttl":60} +2026-10-16T12:44:38.631Z info: [TemplateCache] Template cached {"templateId":"tmpl-8","ttl":60} +2026-10-16T12:44:38.633Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.638Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.642Z info: [TemplateCache] Template cached {"templateId":"tmpl-9","ttl":60} +2026-10-16T12:44:38.645Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.649Z info: [TemplateCache] Template cached {"templateId":"tmpl-10","ttl":60} +2026-10-16T12:44:38.655Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.658Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.663Z info: [TemplateCache] All templates invalidated +2026-10-16T12:44:38.674Z info: [TemplateCache] All templates invalidated +PASS src/services/notification-template-cache.test.ts +2026-10-16T12:44:38.686Z info: [TemplateCache] All templates invalidated +PASS src/__tests__/database-recovery.test.ts (9.456 s) +2026-10-16T12:44:39.274Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:39.275Z info: Database migrations completed +2026-10-16T12:44:39.275Z info: Database initialized successfully {"path":"./data/test-db-recovery.db"} +2026-10-16T12:44:39.277Z info: Database connection closed +PASS src/store/preference-store.test.ts +PASS src/services/backpressure-controller.test.ts +2026-10-16T12:44:42.421Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.429Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.430Z info: Backpressure deactivated: queue recovered {"queueSize":49,"threshold":50,"durationMs":1,"targetThroughputPerSec":100} +2026-10-16T12:44:42.437Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.442Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.446Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.461Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.465Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:42.468Z warn: Backpressure activated: queue saturation detected {"queueSize":101,"threshold":100,"targetThroughputPerSec":10} +2026-10-16T12:44:44.124Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:44.143Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:44.144Z info: Database migrations completed +2026-10-16T12:44:44.144Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:44.155Z info: Notification template created {"templateId":"svc-template"} +2026-10-16T12:44:44.157Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} +2026-10-16T12:44:44.172Z info: Template audit record created {"auditId":1,"templateId":"svc-template","actor":"service-admin","action":"UPDATE"} +2026-10-16T12:44:44.173Z info: [TemplateCache] Template invalidated {"templateId":"svc-template","deleted":1} +2026-10-16T12:44:44.175Z info: Notification template updated {"templateId":"svc-template","actor":"service-admin"} +2026-10-16T12:44:44.176Z info: [TemplateCache] Template cached {"templateId":"svc-template","ttl":60} +2026-10-16T12:44:44.185Z info: Database connection closed +2026-10-16T12:44:44.188Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:44.205Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:44.206Z info: Database migrations completed +2026-10-16T12:44:44.206Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:44.210Z info: Notification template created {"templateId":"tmpl-a"} +2026-10-16T12:44:44.210Z info: [TemplateCache] Template cached {"templateId":"tmpl-a","ttl":60} +2026-10-16T12:44:44.215Z info: Notification template created {"templateId":"tmpl-b"} +2026-10-16T12:44:44.216Z info: [TemplateCache] Template cached {"templateId":"tmpl-b","ttl":60} +2026-10-16T12:44:44.223Z info: Database connection closed +2026-10-16T12:44:44.227Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:44.245Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:44.246Z info: Database migrations completed +2026-10-16T12:44:44.247Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:44.254Z info: Notification template created {"templateId":"del-tmpl"} +2026-10-16T12:44:44.254Z info: [TemplateCache] Template cached {"templateId":"del-tmpl","ttl":60} +2026-10-16T12:44:44.258Z info: [TemplateCache] Template invalidated {"templateId":"del-tmpl","deleted":1} +2026-10-16T12:44:44.259Z info: Notification template deleted {"templateId":"del-tmpl"} +2026-10-16T12:44:44.259Z info: [TemplateCache] Template invalidated {"templateId":"del-tmpl","deleted":0} +2026-10-16T12:44:44.261Z info: Database connection closed +2026-10-16T12:44:44.263Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:44.276Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:44.277Z info: Database migrations completed +2026-10-16T12:44:44.277Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:44.279Z info: Notification template created {"templateId":"render-tmpl"} +2026-10-16T12:44:44.279Z info: [TemplateCache] Template cached {"templateId":"render-tmpl","ttl":60} +2026-10-16T12:44:44.281Z info: Database connection closed +2026-10-16T12:44:44.286Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:44.298Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:44.298Z info: Database migrations completed +2026-10-16T12:44:44.299Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:44.305Z info: Notification template created {"templateId":"render-missing"} +2026-10-16T12:44:44.306Z info: [TemplateCache] Template cached {"templateId":"render-missing","ttl":60} +2026-10-16T12:44:44.329Z info: Database connection closed +2026-10-16T12:44:44.331Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:44.343Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:44.344Z info: Database migrations completed +2026-10-16T12:44:44.344Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:44.347Z info: Notification template created {"templateId":"no-vars"} +PASS src/services/notification-template-service.test.ts +PASS src/store/event-registry.test.ts +2026-10-16T12:44:44.347Z info: [TemplateCache] Template cached {"templateId":"no-vars","ttl":60} +2026-10-16T12:44:44.348Z info: Database connection closed +PASS src/services/retry-statistics.test.ts +PASS src/__tests__/load.test.ts +ts-jest[ts-compiler] (WARN) src/services/providers/provider-health.test.ts:15:56 - +error TS2345: Argument of type '{}' is not assignable to parameter of type 'DiscordConfig'. + Type '{}' is missing the following properties from type 'DiscordConfig': webhookUrl, webhookId + +15 const provider = new DiscordNotificationProvider({}); +   ~~ +src/services/providers/provider-health.test.ts:26:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +26 const provider = new DiscordNotificationProvider({ +   ~ +27 webhookUrl: 'https://discord.com/api/webhooks/123/my-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +28 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +src/services/providers/provider-health.test.ts:42:56 - error TS2345: +Argument of type '{ webhookUrl: string; }' is not assignable to parameter of type 'DiscordConfig'. + Property 'webhookId' is missing in type '{ webhookUrl: string; }' but required in type 'DiscordConfig'. + +42 const provider = new DiscordNotificationProvider({ +   ~ +43 webhookUrl: 'https://discord.com/api/webhooks/123456789/my-super-secret-token', +  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +44 }); +  ~~~~~~~ + + src/types/index.ts:25:3 + 25 webhookId: string; +    ~~~~~~~~~ + 'webhookId' is declared here. +PASS src/services/providers/provider-health.test.ts +2026-10-16T12:44:48.145Z info: Notification analytics aggregator initialized with defaults +FAIL src/__tests__/query-performance.test.ts + ● Test suite failed to run + + Cannot find module 'vitest' from 'src/__tests__/query-performance.test.ts' + + > 1 | import { describe, it, expect } +from "vitest"; // if the repo uses jest, DELETE this line +  | ^ +  2 | import { readFileSync, writeFileSync, mkdirSync } from +"node:fs"; +  3 | import { dirname, resolve } from "node:path"; +  4 | import { performance } from "node:perf_hooks"; + + at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/index.js:1031:11) + at Object. (src/__tests__/query-performance.test.ts:1:1) + +2026-10-16T12:44:49.803Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} +PASS src/utils/batch-validator.test.ts +PASS src/utils/cors-validator.test.ts +PASS src/services/webhook-replay-cache.test.ts +PASS src/services/notification-import-service.test.ts +2026-10-16T12:44:54.358Z info: Bulk notification import complete {"imported":2,"skipped":0,"total":2,"format":"json"} +2026-10-16T12:44:54.366Z info: Bulk notification import complete {"imported":1,"skipped":2,"total":3,"format":"json"} +2026-10-16T12:44:54.371Z info: Bulk notification import complete {"imported":1,"skipped":1,"total":2,"format":"csv"} +PASS src/services/event-provenance.test.ts +2026-10-16T12:44:55.315Z info: Connected to SQLite database {"path":":memory:"} +2026-10-16T12:44:55.329Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:55.330Z info: Database migrations completed +2026-10-16T12:44:55.331Z info: Database initialized successfully {"path":":memory:"} +2026-10-16T12:44:55.357Z info: Database connection closed +2026-10-16T12:44:55.850Z info: Database connection closed +2026-10-16T12:44:55.851Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T12:44:55.852Z warn: Database connection failure detected; attempting recovery {"path":"./data/test-dead-letter-queue.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +2026-10-16T12:44:55.898Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T12:44:55.973Z info: Connected to SQLite database {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T12:44:55.974Z warn: Database connection recovered {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T12:44:55.997Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:55.999Z info: Database migrations completed +2026-10-16T12:44:56.000Z info: Database initialized successfully {"path":"./data/test-dead-letter-queue.db"} +PASS src/__tests__/integration.test.ts +2026-10-16T12:44:56.576Z warn: Event registry at capacity, evicting oldest events {"maxEvents":2,"evicted":1} +2026-10-16T12:44:57.057Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:44:57.058Z info: Database migrations completed +2026-10-16T12:44:57.058Z info: Database initialized successfully {"path":"./data/test-dead-letter-queue.db"} +2026-10-16T12:44:57.077Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"email"} +2026-10-16T12:44:57.121Z warn: Notification moved to dead letter queue {"id":1,"notificationType":"email","failureReason":"permanent failure"} +2026-10-16T12:44:57.122Z info: Notification marked for retry or failed {"id":1,"newStatus":"DEAD_LETTERED","retryCount":1,"maxRetries":1,"nextRetryAt":"2026-10-16T12:45:02.078Z"} +2026-10-16T12:44:57.140Z info: Dead-letter notification requeued {"requestId":"req-2","id":1,"notificationId":1} +2026-10-16T12:44:57.146Z info: Database connection closed +FAIL src/services/dead-letter-queue.test.ts (9.059 s) + ● Dead letter queue processing › moves exhausted notifications into the dead letter queue + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   10 | let repository: ScheduledNotificationRepository; +  11 | + > 12 | beforeEach(async () => { +  | ^ +  13 | if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); +  14 | const dbDir = path.dirname(dbPath); +  15 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + + at src/services/dead-letter-queue.test.ts:12:3 + at Object. (src/services/dead-letter-queue.test.ts:7:1) + + ● Dead letter queue processing › requeues a dead-lettered notification for retry + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-dead-letter-queue.db' + +   22 | afterEach(async () => { +  23 | await db.close(); + > 24 | if (fs.existsSync(dbPath)) +fs.unlinkSync(dbPath); +  | ^ +  25 | }); +  26 | +  27 | it('moves exhausted notifications into the dead letter queue', +async () => { + + at Object. (src/services/dead-letter-queue.test.ts:24:35) + +PASS src/services/provider-registry.test.ts +2026-10-16T12:44:58.477Z info: ProviderRegistry: provider registered {"id":"mock","name":"Mock Provider","capabilities":["RICH_FORMATTING"]} +2026-10-16T12:44:58.485Z info: ProviderRegistry: provider registered {"id":"mockFail","name":"Failing Mock","capabilities":[]} +2026-10-16T12:44:58.489Z info: ProviderRegistry: provider registered {"id":"partial","name":"Partial Mock","capabilities":["RICH_FORMATTING"]} +PASS src/services/event-quarantine.test.ts +PASS src/utils/json-logger-formatter.test.ts (6.682 s) +PASS src/utils/app-version.test.ts (8.666 s) +2026-10-16T12:45:12.293Z info: Connected to SQLite database {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792154712243.db"} +2026-10-16T12:45:19.271Z info: Database connection closed +2026-10-16T12:45:19.273Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":21,\"code\":\"SQLITE_MISUSE\"}"} +2026-10-16T12:45:19.276Z warn: Database connection failure detected; attempting recovery {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792154712243.db","attempt":1,"maxReconnectAttempts":3,"delayMs":100,"error":"SQLITE_MISUSE: Database handle is closed"} +FAIL src/database/query-performance.test.ts (12.076 s) + ● query performance indexes (migration 002) › creates the performance indexes from schema / migrations + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   22 | let dbPath: string; +  23 | + > 24 | beforeAll(async () => { +  | ^ +  25 | dbPath = path.join(os.tmpdir(), +`notify-perf-${Date.now()}.db`); +  26 | db = new Database(dbPath); +  27 | await db.initialize(); + + at src/database/query-performance.test.ts:24:3 + at Object. (src/database/query-performance.test.ts:20:1) + + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   22 | let dbPath: string; +  23 | + > 24 | beforeAll(async () => { +  | ^ +  25 | dbPath = path.join(os.tmpdir(), +`notify-perf-${Date.now()}.db`); +  26 | db = new Database(dbPath); +  27 | await db.initialize(); + + at src/database/query-performance.test.ts:24:3 + at Object. (src/database/query-performance.test.ts:20:1) + +2026-10-16T12:45:19.398Z info: Connected to SQLite database {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792154712243.db"} +2026-10-16T12:45:19.398Z warn: Database connection recovered {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792154712243.db"} +2026-10-16T12:45:19.404Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} +2026-10-16T12:45:19.422Z info: Database migrations completed +2026-10-16T12:45:19.424Z info: Database initialized successfully {"path":"C:\\Users\\NEWOWN~1\\AppData\\Local\\Temp\\notify-perf-1792154712243.db"} +FAIL src/services/notification-metrics-store.test.ts (14.036 s) + ● NotificationMetricsStore › persists and retrieves summarized snapshots + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   21 | let store: NotificationMetricsStore; +  22 | + > 23 | beforeEach(async () => { +  | ^ +  24 | if (fs.existsSync(dbPath)) { +  25 | fs.unlinkSync(dbPath); +  26 | } + + at src/services/notification-metrics-store.test.ts:23:3 + at Object. (src/services/notification-metrics-store.test.ts:18:1) + + ● NotificationMetricsStore › purges snapshots older than the retention window + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-metrics-store.db' + +   33 | await db.close(); +  34 | if (fs.existsSync(dbPath)) { + > 35 | fs.unlinkSync(dbPath); +  | ^ +  36 | } +  37 | }); +  38 | + + at Object. (src/services/notification-metrics-store.test.ts:35:10) + +FAIL src/api/request-duration-logger.test.ts (7.081 s) + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + +PASS src/config/schema-validator.test.ts (7.428 s) +PASS src/services/delivery-receipt-repository.test.ts +PASS src/api/request-size-limiter.test.ts +PASS src/utils/pagination.test.ts +PASS src/services/dead-letter-manager.test.ts +PASS src/services/notification-metrics-runner.test.ts +2026-10-16T12:45:36.173Z info: Payload compressed {"originalSizeBytes":95035,"compressedSizeBytes":1447,"reductionPercent":98} +PASS src/utils/payload-compression.test.ts +2026-10-16T12:45:36.281Z warn: Payload decompression failed, returning original value {"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} +2026-10-16T12:45:36.289Z warn: Payload decompression failed, returning original value {"error":"{\"errno\":-3,\"code\":\"Z_DATA_ERROR\"}","payloadPreview":"__COMPRESSED__:not-valid-base64"} +PASS src/providers/provider-capabilities.test.ts +PASS src/utils/metadata-validator.test.ts +PASS src/services/indexing-reconciliation-engine.test.ts +PASS src/api/cors-validator.test.ts +PASS src/middleware/request-id.test.ts +PASS src/services/job-monitor.test.ts +2026-10-16T12:45:47.561Z error: Job monitor: job failed {"jobId":"job-2","jobName":"discord-delivery","error":"webhook timeout","durationMs":0} +PASS src/utils/config-snapshot.test.ts +PASS src/utils/log-level-resolver.test.ts +PASS src/middleware/security-headers.test.ts +PASS src/utils/notification-version.test.ts +PASS src/services/notification-health-monitor.test.ts +2026-10-16T12:45:51.218Z info: NotificationHealthMonitor started {"intervalMs":30000} +2026-10-16T12:45:51.234Z info: NotificationHealthMonitor stopped +A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due +to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure +that .unref() was called on them. + +Summary of all failing tests +FAIL src/services/webhook-verifier.test.ts (18.67 s) + ● Issue #853 — replay protection at the request boundary › REJECTS a request that carries no timestamp (would +otherwise replay forever) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   599 | }); +  600 | + > 601 | +expect(outcome.authenticated).toBe(false); +  | ^ +  602 | expect(outcome.statusCode).toBe(401); +  603 | +expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); +  604 | }); + + at Object. (src/services/webhook-verifier.test.ts:601:35) + + ● Issue #853 — replay protection at the request boundary › ACCEPTS the same timestamp-less request when legacy mode +is opted into + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   611 | requireTimestamp: false, +  612 | }); + > 613 | +expect(outcome.authenticated).toBe(true); +  | ^ +  614 | }); +  615 | +  616 | it('REJECTS a captured request replayed after the freshness window has +passed', () => { + + at Object. (src/services/webhook-verifier.test.ts:613:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a captured request replayed after the freshness +window has passed + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   626 | maxAgeSeconds: 300, +  627 | }); + > 628 | +expect(first.authenticated).toBe(true); +  | ^ +  629 | +  630 | // Replay one hour later with the exact same captured headers. +  631 | Date.now = () => realNow() + +60 * 60 * 1000; + + at Object. (src/services/webhook-verifier.test.ts:628:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a verbatim replay inside the freshness window via +the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   655 | replayCache, +  656 | }); + > 657 | +expect(first.authenticated).toBe(true); +  | ^ +  658 | +  659 | // Same signature, no Idempotency-Key header — the pre-existing replay +  660 | // defence the attacker would trivially bypass. + + at Object. (src/services/webhook-verifier.test.ts:657:33) + + ● Issue #853 — replay protection at the request boundary › replay cache does not reject a genuinely new request from +the same key + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   693 | }); +  694 | + > 695 | +expect(a.authenticated).toBe(true); +  | ^ +  696 | expect(b.authenticated).toBe(true); +  697 | }); +  698 | + + at Object. (src/services/webhook-verifier.test.ts:695:29) + + ● Issue #853 — replay protection at the request boundary › does not record invalid signatures in the replay cache + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   707 | replayCache, +  708 | }); + > 709 | +expect(bad.authenticated).toBe(false); +  | ^ +  710 | +  711 | // The genuine request must still be accepted afterwards. +  712 | const good = verifyWebhookRequest({ + + at Object. (src/services/webhook-verifier.test.ts:709:31) + + ● Issue #853 — replay protection at the request boundary › REJECTS a non-integer timestamp instead of silently +truncating it + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   727 | }); +  728 | + > 729 | +expect(outcome.authenticated).toBe(false); +  | ^ +  730 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  731 | }); +  732 | + + at Object. (src/services/webhook-verifier.test.ts:729:35) + + ● Issue #853 — replay protection at the request boundary › REJECTS a fractional timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   738 | secrets: SECRETS, +  739 | }); + > 740 | +expect(outcome.authenticated).toBe(false); +  | ^ +  741 | +expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); +  742 | }); +  743 | + + at Object. (src/services/webhook-verifier.test.ts:740:35) + + ● Issue #853 — replay protection at the request boundary › accepts an uppercase hex signature (hex is +case-insensitive) + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: undefined + +   754 | }); +  755 | + > 756 | +expect(outcome.authenticated).toBe(true); +  | ^ +  757 | }); +  758 | +  759 | it('still rejects a genuine signature whose hex case was altered', () +=> { + + at Object. (src/services/webhook-verifier.test.ts:756:35) + + ● Issue #853 — replay protection at the request boundary › still rejects a genuine signature whose hex case was +altered + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   768 | secrets: SECRETS, +  769 | }); + > 770 | +expect(outcome.authenticated).toBe(false); +  | ^ +  771 | }); +  772 | }); +  773 | + + at Object. (src/services/webhook-verifier.test.ts:770:35) + +FAIL src/__tests__/notification-flow-e2e.test.ts (36.358 s) + ● 1. Notification creation › rejects a duplicate idempotency key used with a different payload + + expect(received).rejects.toThrow(expected) + + Expected substring: "Idempotency key reused with different request body" + Received message: "Idempotency key reused with a different request body" + +   131 | if (!isValidRequest) { +  132 | logger.warn('Idempotency key reused with different request body +(replay suspected)', logMeta); + > 133 | throw new +IdempotencyKeyReuseError(); +  | ^ +  134 | } +  135 | +  136 | logger.info('Replay intercepted: returning cached idempotent +response', { + + at IdempotencyKeyService.processWithIdempotency (src/services/idempotency-key-service.ts:133:15) + at NotificationAPI.scheduleNotification (src/services/notification-api.ts:168:9) + at Object. (src/__tests__/notification-flow-e2e.test.ts:242:5) + at Object.toThrow (node_modules/expect/build/index.js:2155:20) + at Object. (src/__tests__/notification-flow-e2e.test.ts:248:15) + + ● 2. Delivery workflows › processes a due notification and transitions it to COMPLETED + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   332 | +  333 | const row = await repository.getById(id); + > 334 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  335 | expect(sendMock).toHaveBeenCalledTimes(1); +  336 | }); +  337 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:334:25) + + ● 2. Delivery workflows › delivers once executeAt elapses mid-run + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   366 | await jest.advanceTimersByTimeAsync(400); +  367 | + > 368 | expect((await repository.getById(id))!. +status).toBe(NotificationStatus.COMPLETED); +  | ^ +  369 | expect(sendMock).toHaveBeenCalledTimes(1); +  370 | }); +  371 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:368:52) + + ● 2. Delivery workflows › writes a SUCCESS execution log entry after delivery + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 0 + Received array: [] + +   385 | [id] +  386 | ); + > 387 | expect(logs).toHaveLength(1); +  | ^ +  388 | expect(logs[0].status).toBe('SUCCESS'); +  389 | expect(logs[0].execution_attempt).toBe(1); +  390 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:387:18) + + ● 2. Delivery workflows › processes a batch of multiple due notifications + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   407 | +  408 | for (const id of ids) { + > 409 | expect((await repository.getById(id))![33 +m.status).toBe(NotificationStatus.COMPLETED); +  | ^ +  410 | } +  411 | expect(sendMock).toHaveBeenCalledTimes(3); +  412 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:409:54) + + ● 2. Delivery workflows › forwards the event and contract config to the Discord service + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: ObjectContaining {"id": "deliver-evt-1"}, ObjectContaining {"address": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"}, Any + + Number of calls: 0 + +   427 | await jest.advanceTimersByTimeAsync(250); +  428 | + > 429 | expect(sendMock).toHaveBeenCalledWith( +  | ^ +  430 | expect.objectContaining({ id: 'deliver-evt-1' }), +  431 | expect.objectContaining({ address: contractConfig.address +}), +  432 | expect.any(String) + + at Object. (src/__tests__/notification-flow-e2e.test.ts:429:22) + + ● 3. Retry scenarios › prevents concurrent duplicate processing via distributed lock + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   576 | await p1; +  577 | + > 578 | expect(sendMock).toHaveBeenCalledTimes(1); +  | ^ +  579 | const row = await db.get<{ +status: string }>( +  580 | 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] +  581 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:578:22) + + ● 3. Retry scenarios › retries a failed delivery on the next scheduler poll cycle + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   609 | +  610 | const row = await repository.getById(id); + > 611 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  612 | expect(sendMock).toHaveBeenCalledTimes(2); +  613 | +  614 | await scheduler.stop(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:611:25) + + ● 4. Failure handling › marks a notification FAILED after exhausting all retries + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   653 | 'SELECT status, retry_count FROM scheduled_notifications WHERE id = +?', [id] +  654 | ); + > 655 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  656 | }); +  657 | +  658 | it('logs a permanent-failure error after max retries are exhausted', +async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:655:25) + + ● 4. Failure handling › logs a permanent-failure error after max retries are exhausted + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"id": 2, "totalAttempts": 3} + Received: "Notification permanently failed after max retries", {"failureType": "unknown", "id": 2, "requestId": +"e2e-req-id", "totalAttempts": 4} + + Number of calls: 1 + +   666 | await retryScheduler.runOnce(); +  667 | + > 668 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  669 | 'Notification permanently failed after max retries', +  670 | expect.objectContaining({ id, totalAttempts: 3 }) +  671 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:668:26) + + ● 4. Failure handling › transitions scheduler-dispatched notification to FAILED when retries run out + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PENDING" + +   727 | +  728 | const row = await repository.getById(id); + > 729 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  730 | +  731 | await scheduler.stop(); +  732 | jest.useRealTimers(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:729:25) + + ● 5. Backpressure and idempotency integration › activates backpressure when queue exceeds saturation threshold + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › calculates a positive processing delay under backpressure + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › deactivates backpressure once the queue drops below recovery +threshold + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › records a backpressure activation event and surfaces it in statistics + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › computes average duration across activation/deactivation pairs + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + ● 5. Backpressure and idempotency integration › cleans up expired idempotency keys after 24 hours + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   48 | const dbDir = path.dirname(TEST_DB_PATH); +  49 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + > 50 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  51 | const db = new +Database(TEST_DB_PATH); +  52 | await db.initialize(); +  53 | return db; + + at setupDb (src/__tests__/notification-flow-e2e.test.ts:50:39) + at Object. (src/__tests__/notification-flow-e2e.test.ts:801:16) + + + ● Test suite failed to run + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-notification-flow-e2e.db' + +   632 | afterAll(async () => { +  633 | await db.close(); + > 634 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  | ^ +  635 | }); +  636 | +  637 | beforeEach(async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:634:41) + + ● Test suite failed to run + + TypeError: Cannot read properties of undefined (reading 'close') + +   814 | +  815 | afterAll(async () => { + > 816 | await db.close(); +  | ^ +  817 | if (fs.existsSync(TEST_DB_PATH)) +fs.unlinkSync(TEST_DB_PATH); +  818 | }); +  819 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:816:14) + +FAIL src/__tests__/blockchain-to-notification-e2e.test.ts (29.081 s) + ● Stage 6 – NotificationScheduler delivers scheduled notifications › multiple due notifications in a batch are all +delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:776:5) + + ● Stage 7 – Full pipeline integration: blockchain event → delivered notification › multiple events from different +contracts are all delivered + + waitUntil: condition not met within timeout + +   140 | await wait(intervalMs); +  141 | } + > 142 | throw new Error('waitUntil: condition +not met within timeout'); +  | ^ +  143 | } +  144 | +  145 | // ═══════════════════════════════════════════════════════════════════════════════ + + at waitUntil (src/__tests__/blockchain-to-notification-e2e.test.ts:142:9) + at Object. (src/__tests__/blockchain-to-notification-e2e.test.ts:1111:5) + +FAIL src/api/events-server.test.ts (11.579 s) + ● GET /health › returns the configured network without exposing sensitive configuration + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 503 + +   179 | const body = res.body as +Record<string, unknown>; +  180 | + > 181 | expect(res.status).toBe(200); +  | ^ +  182 | expect(body.network).toBe('testnet'); +  183 | +expect(body).not.toHaveProperty('stellarNetworkPassphrase'); +  184 | +expect(JSON.stringify(body)).not.toContain('secret +passphrase'); + + at Object. (src/api/events-server.test.ts:181:24) + + ● POST /api/webhooks › rejects a legacy signature when a timestamp IS provided (anti-replay: timestamp binding) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   301 | }); +  302 | + > 303 | expect(status).toBe(401); +  | ^ +  304 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  305 | expect((body as any).success).toBe(false); +  306 | }); + + at Object. (src/api/events-server.test.ts:303:20) + + ● POST /api/webhooks › rejects a timestamp-bound signature when the timestamp header is removed (anti-replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   321 | }); +  322 | + > 323 | expect(status).toBe(401); +  | ^ +  324 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  325 | }); +  326 | + + at Object. (src/api/events-server.test.ts:323:20) + + ● POST /api/webhooks › rejects a webhook with an expired timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   337 | }); +  338 | + > 339 | expect(status).toBe(401); +  | ^ +  340 | expect((body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  341 | }); +  342 | + + at Object. (src/api/events-server.test.ts:339:20) + + ● POST /api/webhooks › rejects a webhook with a forged timestamp (signed for ts=A, header claims ts=B) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | }); +  355 | + > 356 | expect(status).toBe(401); +  | ^ +  357 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  358 | }); +  359 | + + at Object. (src/api/events-server.test.ts:356:20) + + ● POST /api/webhooks › rejects a webhook with an invalid signature (wrong secret) + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   370 | }); +  371 | + > 372 | expect(status).toBe(401); +  | ^ +  373 | expect((body as +any).code).toBe('AUTH_INVALID_SIGNATURE'); +  374 | expect((body as any).success).toBe(false); +  375 | expect((body as +any).error.message).toBe('Invalid signature'); + + at Object. (src/api/events-server.test.ts:372:20) + + ● POST /api/webhooks › rejects when signature header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   384 | }); +  385 | + > 386 | expect(status).toBe(401); +  | ^ +  387 | expect((body as +any).code).toBe('AUTH_MISSING_SIGNATURE'); +  388 | expect((body as any).success).toBe(false); +  389 | expect((body as +any).error.message).toBe('Missing signature header'); + + at Object. (src/api/events-server.test.ts:386:20) + + ● POST /api/webhooks › rejects when key-id header is missing + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   401 | }); +  402 | + > 403 | expect(status).toBe(401); +  | ^ +  404 | expect((body as +any).code).toBe('AUTH_MISSING_KEY_ID'); +  405 | expect((body as any).success).toBe(false); +  406 | expect((body as +any).error.message).toBe('Missing key-id header'); + + at Object. (src/api/events-server.test.ts:403:20) + + ● POST /api/webhooks › rejects when key-id is unknown + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   419 | }); +  420 | + > 421 | expect(status).toBe(401); +  | ^ +  422 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  423 | expect((body as any).success).toBe(false); +  424 | expect((body as +any).error.message).toBe('Unknown key-id'); + + at Object. (src/api/events-server.test.ts:421:20) + + ● POST /api/webhooks › rejects when no webhook secrets are configured + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   437 | }); +  438 | + > 439 | expect(status).toBe(401); +  | ^ +  440 | expect((body as +any).code).toBe('AUTH_UNKNOWN_KEY_ID'); +  441 | }); +  442 | + + at Object. (src/api/events-server.test.ts:439:20) + + ● POST /api/webhooks › logs authentication failures with structured context + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: undefined + +   451 | +  452 | expect(logger.warn).toHaveBeenCalled(); + > 453 | expect((body as +any).success).toBe(false); +  | ^ +  454 | expect((body as +any).error.message).toBe('Missing signature header'); +  455 | }); +  456 | + + at Object. (src/api/events-server.test.ts:453:35) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #1 — exact same request sent twice returns +cached response on second call (200 not 202) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   727 | }); +  728 | + > 729 | expect(second.status).toBe(200); +  | ^ +  730 | expect((second.body as +any).replay).toBe(true); +  731 | expect((second.body as +any).status).toBe('accepted'); +  732 | } finally { + + at Object. (src/api/events-server.test.ts:729:29) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #3 — three identical requests → #1=202, +#2=200(replay), #3=200(replay) + + expect(received).toBe(expected) // Object.is equality + + Expected: 200 + Received: 409 + +   800 | expect(r1.status).toBe(202); +  801 | expect((r1.body as +any).replay).toBe(false); + > 802 | expect(r2.status).toBe(200); +  | ^ +  803 | expect((r2.body as +any).replay).toBe(true); +  804 | expect(r3.status).toBe(200); +  805 | expect((r3.body as +any).replay).toBe(true); + + at Object. (src/api/events-server.test.ts:802:25) + + ● REPLAY ATTACK PROTECTION — /api/webhooks with Idempotency-Key › REPLAY #4 — Layer 2 timestamp binding rejects a +captured request replayed without valid signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   830 | }); +  831 | + > 832 | expect(replay.status).toBe(401); +  | ^ +  833 | expect((replay.body as +any).code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  834 | } finally { +  835 | await closeServer(server); + + at Object. (src/api/events-server.test.ts:832:29) + +FAIL src/services/event-subscriber.test.ts + ● EventSubscriber › notification expiration (Task 3: Requirements 2.1, 2.2, 2.3) › logs expiration with timestamp +details + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Skipping expired notification", ObjectContaining {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": Any, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "receivedAt": 1792067878768} + Received + 1: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": +10000, "error": "server.getLatestLedger is not a function"} + 2: "Event skipped due to expiration", {"currentTime": 1792154279951, "eventId": "expired-details", +"eventType": "TaskCreated", "receivedAt": 1792067878768} + 3: "Skipping expired notification", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "currentTime": 1792154279951, "eventId": "expired-details", +"eventName": "TaskCreated", "reason": "expired", "requestId": "77ed391b"} + + Number of calls: 3 + +   816 | await (subscriber as any).checkForEvents(); +  817 | + > 818 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  819 | 'Skipping expired notification', +  820 | expect.objectContaining({ +  821 | contractAddress: contractConfig.address, + + at Object. (src/services/event-subscriber.test.ts:818:31) + +FAIL src/services/retry-policy-integration.test.ts + ● RetryScheduler — retry policy (#842) › permanent failures retire the row end-to-end › marks a 404 FAILED and stops +re-selecting it on the next poll + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   422 | +  423 | const row = (await +repository.getById(id))!; + > 424 | +expect(row.status).toBe(NotificationStatus.FAILED); +  | ^ +  425 | expect(row.nextRetryAt).toBeNull(); +  426 | +  427 | // The decisive assertion: another poll must not hand the row back. + + at Object. (src/services/retry-policy-integration.test.ts:424:26) + + ● NotificationRetryQueue — retry policy (#842) › does not requeue an item whose delivery threw a permanent failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   641 | +  642 | // Attempted once, then dropped — no requeue despite 4 attempts of budget left. + > 643 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  644 | expect(queue.size()).toBe(0); +  645 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  646 | expect(logger.error).toHaveBeenCalledWith( + + at Object. (src/services/retry-policy-integration.test.ts:643:28) + + ● NotificationRetryQueue — retry policy (#842) › keeps requeueing an item whose delivery threw a transient failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   666 | jest.advanceTimersByTime(20); +  667 | await flush(); + > 668 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  669 | +  670 | jest.advanceTimersByTime(40); +  671 | await flush(); + + at Object. (src/services/retry-policy-integration.test.ts:668:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a policy attempt ceiling that is lower than maxRetries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   690 | await flush(); +  691 | + > 692 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  693 | expect(queue.size()).toBe(0); +  694 | queue.stop(); +  695 | }); + + at Object. (src/services/retry-policy-integration.test.ts:692:28) + + ● NotificationRetryQueue — retry policy (#842) › honours a narrowed eligible failure set + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   712 | await flush(); +  713 | + > 714 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  715 | expect(queue.size()).toBe(0); +  716 | +expect(queue.getMetrics().totalSkippedPermanent).toBe(1); +  717 | queue.stop(); + + at Object. (src/services/retry-policy-integration.test.ts:714:28) + +FAIL src/config.test.ts + ● Config validation › loads default values when optional environment variables are omitted + + expect(received).toMatchObject(expected) + + - Expected - 1 + + Received + 7 + + @@ -26,11 +26,17 @@ + "eventsApiPort": 8787, + "maxReconnectAttempts": 5, + "pollIntervalMs": 30000, + "reconnectDelayMs": 5000, + "retryQueue": Object { + - "baseDelayMs": 5000, + + "backoff": Object { + + "initialDelayMs": 5000, + + "jitter": true, + + "maxDelayMs": 3600000, + "maxRetries": 5, + + "multiplier": 2, + + }, + + "processIntervalMs": 5000, + }, + "stellarNetwork": "testnet", + "stellarRpcUrl": "https://soroban-testnet.stellar.org:443", + } + +   104 | const config = loadConfig(); +  105 | + > 106 | expect(config).toMatchObject({ +  | ^ +  107 | stellarNetwork: 'testnet', +  108 | stellarRpcUrl: 'https://soroban-testnet.stellar.org:443', +  109 | contractAddresses: [{ address: +'CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', events: ['*'] +}], + + at Object. (src/config.test.ts:106:20) + +FAIL src/tests/template-api-integration.test.ts + ● Template API Integration Tests › POST /api/templates - Create Template › should create valid template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject template with invalid syntax + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject duplicate unique key + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject missing required fields + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates - Create Template › should reject script injection in template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should list all templates + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by channel type + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should filter by active status + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates - List Templates › should support pagination + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should get template by ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id - Get Template › should reject invalid ID + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should update template successfully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should reject invalid template update + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › PUT /api/templates/:id - Update Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should soft delete (deactivate) +template by default + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should hard delete when hard=true + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › DELETE /api/templates/:id - Delete Template › should return 404 for non-existent +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should render template with all +variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should reject rendering with +missing variables + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should handle XSS attempts with +escaping + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for inactive +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › POST /api/templates/render - Render Template › should return 400 for missing +template + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/stats - Overview Statistics › should return overview statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › GET /api/templates/:id/stats - Template Usage Statistics › should return usage +statistics + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle nested property rendering + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle default values + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should reject prototype pollution attempts + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should validate channel-specific requirements + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle special characters in context + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Edge Cases and Security › should handle empty context gracefully + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle large template body + + SQLITE_ERROR: no such table: template_usage_log + + ● Template API Integration Tests › Performance and Scalability › should handle many variables + + SQLITE_ERROR: no such table: template_usage_log + +FAIL src/services/retry-deduplication.test.ts (15.247 s) + ● Retry Deduplication - Edge Cases › should count max-retry exhausted notification as exactly 1 failure + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   89 | +  90 | // CRITICAL: Should count as exactly 1 failure, not 3 + > 91 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  92 | expect(metrics.permanentFailures).toBe(1); +  93 | expect(metrics.successfulFirstAttempt).toBe(0); +  94 | expect(metrics.successfulAfterRetry).toBe(0); + + at Object. (src/services/retry-deduplication.test.ts:91:40) + + ● Retry Deduplication - Edge Cases › should handle success on final retry attempt correctly + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 1 + +   179 | expect(metrics.successfulAfterRetry).toBe(1); +// ← Success on final attempt +  180 | expect(metrics.permanentFailures).toBe(0); + > 181 | +expect(metrics.totalRetryAttempts).toBe(2); +  | ^ +  182 | }); +  183 | +  184 | /** + + at Object. (src/services/retry-deduplication.test.ts:181:40) + + ● Retry Deduplication - Edge Cases › should accurately deduplicate in high-volume mixed-outcome scenario + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   186 |  * Simulates realistic production load with various retry patterns +  187 |  */ + > 188 | it('should accurately deduplicate in high-volume mixed-outcome +scenario', async () => { +  | ^ +  189 | const outcomes = { +  190 | immediateSuccess: 0, +  191 | retrySuccess: 0, + + at src/services/retry-deduplication.test.ts:188:5 + at Object. (src/services/retry-deduplication.test.ts:13:9) + + ● Retry Deduplication - Edge Cases › should exclude CANCELLED notifications from metrics + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 2 + +   384 | +  385 | // Should only count the completed notification + > 386 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  387 | expect(metrics.successfulFirstAttempt).toBe(1); +  388 | }); +  389 | + + at Object. (src/services/retry-deduplication.test.ts:386:40) + + ● Retry Deduplication - Edge Cases › should handle notifications without execution log entries + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 3 + +   407 | +  408 | // Should still count the notification + > 409 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  410 | // Without log entry, final_execution_status will be NULL +  411 | // The query should handle this gracefully +  412 | }); + + at Object. (src/services/retry-deduplication.test.ts:409:40) + + ● Retry Deduplication - Edge Cases › should handle concurrent retry patterns without cross-contamination + + expect(received).toBe(expected) // Object.is equality + + Expected: 2 + Received: 5 + +   482 | +  483 | // Each notification should be counted exactly once + > 484 | +expect(metrics.totalNotifications).toBe(2); +  | ^ +  485 | expect(metrics.successfulFirstAttempt).toBe(0); +  486 | expect(metrics.successfulAfterRetry).toBe(2); +  487 | expect(metrics.totalRetryAttempts).toBe(3); // +N1: 1 retry, N2: 2 retries + + at Object. (src/services/retry-deduplication.test.ts:484:40) + + ● Retry Deduplication - Edge Cases › should accurately track notifications with high retry counts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 6 + +   527 | const metrics = await +repository.getExecutionMetrics(); +  528 | + > 529 | +expect(metrics.totalNotifications).toBe(1); +  | ^ +  530 | expect(metrics.successfulFirstAttempt).toBe(0); +  531 | expect(metrics.successfulAfterRetry).toBe(1); +// Still just 1 success +  532 | expect(metrics.totalRetryAttempts).toBe(9); // +9 retries before success + + at Object. (src/services/retry-deduplication.test.ts:529:40) + + ● Retry Deduplication - Edge Cases › should provide accurate retry distribution without double-counting + + expect(received).toBe(expected) // Object.is equality + + Expected: 5 + Received: 10 + +   590 | // Verify distribution matches expected patterns +  591 | const retries0 = distribution.find(d => +d.retryCount === 0); + > 592 | +expect(retries0?.successCount).toBe(5); +  | ^ +  593 | expect(retries0?.failureCount).toBe(0); +  594 | +  595 | const retries1 = distribution.find(d => +d.retryCount === 1); + + at Object. (src/services/retry-deduplication.test.ts:592:36) + +FAIL src/__tests__/smoke/notification-pipeline.smoke.test.ts + ● Notification Pipeline Smoke Test › End-to-End Pipeline › should process event with complex data payload + + TypeError: Cannot read properties of undefined (reading 'fromString') + +   584 | case 'address': +  585 | value = +StellarSDK.xdr.ScVal.scvAddress( + > 586 | +StellarSDK.Address.fromString(contractAddress).toScAddress() +  | ^ +  587 | ); +  588 | break; +  589 | case 'void': + + at createMockContractEvent (src/__tests__/smoke/notification-pipeline.smoke.test.ts:586:28) + at Object. (src/__tests__/smoke/notification-pipeline.smoke.test.ts:213:28) + +FAIL src/services/providers/provider-capabilities.test.ts + ● DiscordNotificationProvider › delivers successfully when webhook returns 2xx + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   156 | const result = await +provider.deliver(makePayload({ targetRecipient: discordConfig.webhookUrl })); +  157 | + > 158 | expect(result.success).toBe(true); +  | ^ +  159 | expect(result.degradedCapabilities).toHaveLength(0); +  160 | }); +  161 | + + at Object. (src/services/providers/provider-capabilities.test.ts:158:28) + + ● DiscordNotificationProvider › returns success:false when webhook returns 4xx + + expect(received).toContain(expected) // indexOf + + Expected substring: "HTTP 400" + Received string: "Cannot read properties of undefined (reading 'get')" + +   172 | +  173 | expect(result.success).toBe(false); + > 174 | expect(result.errorMessage).toContain('HTTP +400'); +  | ^ +  175 | }); +  176 | +  177 | it('lists unsupported features in degradedCapabilities but still +delivers', async () => { + + at Object. (src/services/providers/provider-capabilities.test.ts:174:33) + + ● DiscordNotificationProvider › lists unsupported features in degradedCapabilities but still delivers + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   190 | ); +  191 | + > 192 | expect(result.success).toBe(true); +  | ^ +  193 | expect(result.degradedCapabilities).toContain(ProviderCapability[39 +m.NATIVE_SCHEDULING); +  194 | expect(result.degradedCapabilities).not.toContain(Provider +Capability.RICH_FORMATTING); +  195 | }); + + at Object. (src/services/providers/provider-capabilities.test.ts:192:28) + +FAIL src/services/discord-notification.test.ts (5.658 s) + ● DiscordNotificationService › formatEventMessage › should handle exact field value length boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + Received: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..." + +   384 | const body = +JSON.parse(options.body); +  385 | const valueField = +body.embeds[0].fields.find((f: any) => f.name +=== 'Value'); + > 386 | expect(valueField.value).toBe(exactValue); +  | ^ +  387 | }); +  388 | }); +  389 | }); + + at Object. (src/services/discord-notification.test.ts:386:32) + +FAIL src/services/notification-expiration.test.ts + ● NotificationExpirationService › isExpired() › should return false for events at exact expiration boundary + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   98 | const result = service.isExpired(event); +  99 | // At exact boundary, should not be expired (> not >=) + > 100 | expect(result).toBe(false); +  | ^ +  101 | }); +  102 | +  103 | it('should return false when expiration is disabled', () => +{ + + at Object. (src/services/notification-expiration.test.ts:100:22) + +FAIL src/database/database-transaction.test.ts + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › commits the status change +and creates a DLQ entry on permanent failure + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   443 | [id] +  444 | ); + > 445 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  446 | +  447 | const dlqCount = await countRows(db, +'dead_letter_queue'); +  448 | expect(dlqCount).toBe(1); + + at Object. (src/database/database-transaction.test.ts:445:25) + + ● ScheduledNotificationRepository.markAsFailedOrRetry() — partial-state documentation › documents that the status +update persists even when the DLQ insert throws + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   495 | [id] +  496 | ); + > 497 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  498 | +  499 | // DLQ has NO record — partial state is present +  500 | const dlqCount = await countRows(db, +'dead_letter_queue'); + + at Object. (src/database/database-transaction.test.ts:497:25) + +FAIL src/services/archive.test.ts + ● handleArchiveRequest › GET /api/archive returns paginated results + + expect(received).toHaveProperty(path) + + Expected path: "records" + Received path: [] + + Received value: {"data": {"itemCount": 0, "limit": 20, "offset": 0, "records": [], "total": 0, "totalPages": 0}, +"success": true} + +   462 | expect(res._status).toBe(200); +  463 | const body = +JSON.parse(res._body); + > 464 | expect(body).toHaveProperty('records'); +  | ^ +  465 | expect(body).toHaveProperty('total'); +  466 | }); +  467 | + + at Object. (src/services/archive.test.ts:464:18) + + ● handleArchiveRequest › GET /api/archive/:id returns the record when it exists + + expect(received).toBe(expected) // Object.is equality + + Expected: 42 + Received: undefined + +   498 | expect(handled).toBe(true); +  499 | expect(res._status).toBe(200); + > 500 | +expect(JSON.parse(res._body).id).toBe(42); +  | ^ +  501 | }); +  502 | +  503 | it('POST /api/archive/run returns 503 when service not provided', +async () => { + + at Object. (src/services/archive.test.ts:500:38) + + ● handleArchiveRequest › POST /api/archive/run triggers cycle when service is provided + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: undefined + +   520 | +expect(fakeService.runCycle).toHaveBeenCalledTimes(1); +  521 | const body = +JSON.parse(res._body); + > 522 | expect(body.archived).toBe(1); +  | ^ +  523 | }); +  524 | }); +  525 | + + at Object. (src/services/archive.test.ts:522:27) + +FAIL src/services/webhook-retry-helper.test.ts + ● sendWebhookWithRetry › classifyWebhookFailure › does not treat a message mentioning "timeout" as a timeout + + expect(received).toBe(expected) // Object.is equality + + Expected: "network" + Received: "timeout" + +   446 | // Only the error name marks an abort; a generic message must not be +  447 | // mistaken for a timeout, otherwise retry/observability cannot tell them apart. + > 448 | expect(classifyWebhookFailure(undefined, new +Error('timeout'))).toBe('network'); +  | ^ +  449 | }); +  450 | +  451 | it('classifies retryable HTTP statuses', () => { + + at Object. (src/services/webhook-retry-helper.test.ts:448:71) + +FAIL src/__tests__/notification-delivery-lifecycle.e2e.test.ts (11.415 s) + ● Notification delivery lifecycle (e2e) › failure and retry › enqueues to retry queue when immediate delivery fails + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   182 | +  183 | // 1 initial + 1 retry = 2 total calls + > 184 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  185 | expect(retryQueue.size()).toBe(0); +  186 | +  187 | retryQueue.stop(); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:184:25) + + ● Notification delivery lifecycle (e2e) › failure and retry › logs permanent failure after retry exhaustion + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 2 + Received number of calls: 1 + +   216 | +  217 | // 1 initial + 1 retry = 2 total + > 218 | +expect(fetchMock).toHaveBeenCalledTimes(2); +  | ^ +  219 | expect(retryQueue.size()).toBe(0); +  220 | expect(logger.error).toHaveBeenCalledWith( +  221 | 'Notification permanently failed after max retries', + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:218:25) + + ● Notification delivery lifecycle (e2e) › event topic rendering › handles numeric values in event payload + + TypeError: StellarSDK.xdr.Uint64 is not a constructor + +   270 | const service = new +DiscordNotificationService(discordConfig); +  271 | const event = makeEvent({ + > 272 | value: +xdr.ScVal.scvU64(new +StellarSDK.xdr.Uint64(42n)), +  | ^ +  273 | }); +  274 | +  275 | await service.sendEventNotification(event, +contractCfg); + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:272:33) + + ● Notification delivery lifecycle (e2e) › network errors › returns false and logs error when fetch throws + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   294 | ); +  295 | + > 296 | expect(result).toBe(false); +  | ^ +  297 | expect(logger.error).toHaveBeenCalledWith( +  298 | 'Discord webhook request error', +  299 | expect.objectContaining({ + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:296:22) + + ● Notification delivery lifecycle (e2e) › network errors › returns false on timeout (abort) + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   322 | contractCfg +  323 | ); + > 324 | expect(result).toBe(false); +  | ^ +  325 | }); +  326 | }); +  327 | + + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:324:22) + + ● Notification delivery lifecycle (e2e) › multi-channel independent queues › each channel maintains its own retry +queue independently + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   373 | // ========================================================================= +  374 | describe('multi-channel independent queues', () => { + > 375 | it('each channel maintains its own retry queue +independently', async () => { +  | ^ +  376 | const channels = ['alerts', +'ops', 'audit'].map((name) => { +  377 | const cfg: DiscordConfig = { +  378 | webhookUrl: +`https://discord.com/api/webhooks/${name}/token`, + + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:375:5 + at src/__tests__/notification-delivery-lifecycle.e2e.test.ts:374:3 + at Object. (src/__tests__/notification-delivery-lifecycle.e2e.test.ts:74:1) + +FAIL src/api/auth-protected-endpoints.test.ts + ● POST /api/webhooks (HMAC signature auth) › rejects missing credentials (no signature) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   310 | it('rejects missing credentials (no signature) with 401', +async () => { +  311 | const res = await post({ 'Content-Type': +'application/json' }); + > 312 | expect(res.status).toBe(401); +  | ^ +  313 | +expect(res.body.code).toBe('AUTH_MISSING_SIGNATURE'); +  314 | }); +  315 | + + at Object. (src/api/auth-protected-endpoints.test.ts:312:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a signature without a key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   316 | it('rejects a signature without a key id with 401', async () +=> { +  317 | const res = await post(signedHeaders({ keyId: +null })); + > 318 | expect(res.status).toBe(401); +  | ^ +  319 | +expect(res.body.code).toBe('AUTH_MISSING_KEY_ID'); +  320 | }); +  321 | + + at Object. (src/api/auth-protected-endpoints.test.ts:318:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects invalid credentials (wrong secret) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   322 | it('rejects invalid credentials (wrong secret) with 401', +async () => { +  323 | const res = await post(signedHeaders({ secret: +'whsec_attacker_guess' })); + > 324 | expect(res.status).toBe(401); +  | ^ +  325 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  326 | }); +  327 | + + at Object. (src/api/auth-protected-endpoints.test.ts:324:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects an unknown key id with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   328 | it('rejects an unknown key id with 401', async () +=> { +  329 | const res = await post(signedHeaders({ keyId: +'partner-unknown' })); + > 330 | expect(res.status).toBe(401); +  | ^ +  331 | +expect(res.body.code).toBe('AUTH_UNKNOWN_KEY_ID'); +  332 | }); +  333 | + + at Object. (src/api/auth-protected-endpoints.test.ts:330:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a malformed signature with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   334 | it('rejects a malformed signature with 401', async () +=> { +  335 | const res = await post(signedHeaders({ signature: +'md5=abc' })); + > 336 | expect(res.status).toBe(401); +  | ^ +  337 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE_FORMAT'); +  338 | }); +  339 | + + at Object. (src/api/auth-protected-endpoints.test.ts:336:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects expired credentials (stale timestamp) with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   341 | const stale = +String(Math.floor(Date.now() / 1000) - +301); +  342 | const res = await post(signedHeaders({ timestamp: +stale })); + > 343 | expect(res.status).toBe(401); +  | ^ +  344 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  345 | }); +  346 | + + at Object. (src/api/auth-protected-endpoints.test.ts:343:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects timestamps too far in the future with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   348 | const future = +String(Math.floor(Date.now() / 1000) + +3600); +  349 | const res = await post(signedHeaders({ timestamp: +future })); + > 350 | expect(res.status).toBe(401); +  | ^ +  351 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  352 | }); +  353 | + + at Object. (src/api/auth-protected-endpoints.test.ts:350:24) + + ● POST /api/webhooks (HMAC signature auth) › rejects a non-numeric timestamp with 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   354 | it('rejects a non-numeric timestamp with 401', async () +=> { +  355 | const res = await post(signedHeaders({ timestamp: +'yesterday' })); + > 356 | expect(res.status).toBe(401); +  | ^ +  357 | +expect(res.body.code).toBe('AUTH_TIMESTAMP_EXPIRED'); +  358 | }); +  359 | + + at Object. (src/api/auth-protected-endpoints.test.ts:356:24) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects a tampered body signed for +different content + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   367 | it('rejects a tampered body signed for different content', +async () => { +  368 | const res = await post(signedHeaders(), +JSON.stringify({ event: 'notification.created', id: +'n-2' })); + > 369 | expect(res.status).toBe(401); +  | ^ +  370 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  371 | }); +  372 | + + at Object. (src/api/auth-protected-endpoints.test.ts:369:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects replaying a signature with a +refreshed timestamp + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   375 | const oldSignature = computeWebhookSignature(body, +WEBHOOK_SECRET, oldTs); +  376 | const res = await post(signedHeaders({ signature: +oldSignature, timestamp: nowSeconds() })); + > 377 | expect(res.status).toBe(401); +  | ^ +  378 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  379 | }); +  380 | + + at Object. (src/api/auth-protected-endpoints.test.ts:377:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects stripping the timestamp from a +timestamp-bound signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   383 | const signature = computeWebhookSignature(body, +WEBHOOK_SECRET, ts); +  384 | const res = await post(signedHeaders({ signature, +timestamp: null })); + > 385 | expect(res.status).toBe(401); +  | ^ +  386 | +expect(res.body.code).toBe('AUTH_INVALID_SIGNATURE'); +  387 | }); +  388 | + + at Object. (src/api/auth-protected-endpoints.test.ts:385:26) + + ● POST /api/webhooks (HMAC signature auth) › unauthorized access attempts › rejects an API key presented instead of +a signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   389 | it('rejects an API key presented instead of a signature', +async () => { +  390 | const res = await post({ 'Content-Type': +'application/json', 'X-API-Key': PRIMARY_KEY }); + > 391 | expect(res.status).toBe(401); +  | ^ +  392 | }); +  393 | +  394 | it('does not echo the secret or expected signature in the 401 body', +async () => { + + at Object. (src/api/auth-protected-endpoints.test.ts:391:26) + +FAIL src/tests/notification-scheduler-refactored.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\src\tests\notification-scheduler-refactored.test.ts:271 + const pastLock = notification_fixture_builder_1.NotificationFixtureBuilder.dates.past(1000); + ^ + + SyntaxError: Identifier 'pastLock' has already been declared + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/discord-notification-refactored.test.ts (80.619 s) + ● DiscordNotificationService (Refactored) › sendEventNotification › should send event notification successfully + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   33 | +  34 | describe('sendEventNotification', () => { + > 35 | it('should send event notification successfully', +async () => { +  | ^ +  36 | mockFetch.mockResolvedValueOnce({ ok: true }); +  37 | +  38 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:35:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle webhook failure + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   64 | }); +  65 | + > 66 | it('should handle webhook failure', +async () => { +  | ^ +  67 | mockFetch.mockResolvedValueOnce({ +  68 | ok: false, +  69 | status: 400, + + at src/services/discord-notification-refactored.test.ts:66:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › sendEventNotification › should handle network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   87 | }); +  88 | + > 89 | it('should handle network error', async +() => { +  | ^ +  90 | mockFetch.mockRejectedValueOnce(new Error('Network +error')); +  91 | +  92 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:89:5 + at src/services/discord-notification-refactored.test.ts:34:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › skips the webhook call for a duplicate event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   106 | +  107 | describe('duplicate detection', () => { + > 108 | it('skips the webhook call for a duplicate +event', async () => { +  | ^ +  109 | mockFetch.mockResolvedValue({ ok: true }); +  110 | +  111 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:108:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › logs a duplicate detection event + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   128 | }); +  129 | + > 130 | it('logs a duplicate detection event', +async () => { +  | ^ +  131 | mockFetch.mockResolvedValue({ ok: true }); +  132 | +  133 | const mockLoggerModule = +jest.requireMock('../utils/logger').default; + + at src/services/discord-notification-refactored.test.ts:130:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › allows the same event id on a different contract +through + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   155 | }); +  156 | + > 157 | it('allows the same event id on a different contract +through', async () => { +  | ^ +  158 | mockFetch.mockResolvedValue({ ok: true }); +  159 | +  160 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:157:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › does not mark an event as sent when the webhook +call fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   183 | }); +  184 | + > 185 | it('does not mark an event as sent when the webhook call +fails', async () => { +  | ^ +  186 | mockFetch +  187 | .mockResolvedValueOnce({ +  188 | ok: false,  + + at src/services/discord-notification-refactored.test.ts:185:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › duplicate detection › accepts an injected deduplicator + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   212 | }); +  213 | + > 214 | it('accepts an injected deduplicator', +async () => { +  | ^ +  215 | mockFetch.mockResolvedValue({ ok: true }); +  216 | +  217 | const deduplicator = new +NotificationDeduplicator(); + + at src/services/discord-notification-refactored.test.ts:214:5 + at src/services/discord-notification-refactored.test.ts:107:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should format event with string value correctly + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   257 | +  258 | describe('formatEventMessage', () => { + > 259 | it('should format event with string value +correctly', async () => { +  | ^ +  260 | mockFetch.mockResolvedValueOnce({ ok: true }); +  261 | +  262 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:259:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate long string values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   281 | }); +  282 | + > 283 | it('should truncate long string values', +async () => { +  | ^ +  284 | mockFetch.mockResolvedValueOnce({ ok: true }); +  285 | +  286 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:283:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle symbol type values + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   306 | }); +  307 | + > 308 | it('should handle symbol type values', +async () => { +  | ^ +  309 | mockFetch.mockResolvedValueOnce({ ok: true }); +  310 | +  311 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:308:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should preserve short messages without truncation + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   329 | }); +  330 | + > 331 | it('should preserve short messages without +truncation', async () => { +  | ^ +  332 | mockFetch.mockResolvedValueOnce({ ok: true }); +  333 | +  334 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:331:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should truncate oversized embed titles + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   352 | }); +  353 | + > 354 | it('should truncate oversized embed titles', +async () => { +  | ^ +  355 | mockFetch.mockResolvedValueOnce({ ok: true }); +  356 | +  357 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:354:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should enforce total embed size limit + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   377 | }); +  378 | + > 379 | it('should enforce total embed size limit', +async () => { +  | ^ +  380 | mockFetch.mockResolvedValueOnce({ ok: true }); +  381 | +  382 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:379:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + + ● DiscordNotificationService (Refactored) › formatEventMessage › should handle exact field value length boundary + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   403 | }); +  404 | + > 405 | it('should handle exact field value length +boundary', async () => { +  | ^ +  406 | mockFetch.mockResolvedValueOnce({ ok: true }); +  407 | +  408 | const service = new +DiscordNotificationService(mockConfig); + + at src/services/discord-notification-refactored.test.ts:405:5 + at src/services/discord-notification-refactored.test.ts:258:3 + at Object. (src/services/discord-notification-refactored.test.ts:24:1) + +FAIL src/services/notification-failure-scenarios.test.ts + ● Notification Failure Scenarios (#793) › 1. Provider Timeout › handles provider timeout in WebhookDeliveryService +via AbortError + + expect(received).toMatch(expected) + + Expected pattern: /timed out/i + Received string: "DNS resolution returned no addresses for hostname: api.example.com" + +   54 | +  55 | expect(result.success).toBe(false); + > 56 | expect(result.errorReason).toMatch(/timed +out/i); +  | ^ +  57 | }); +  58 | +  59 | it('tracks timeoutCount in DiscordNotificationService on AbortError', +async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:56:34) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles corrupted/empty HTTP responses gracefully + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   91 | +  92 | expect(result.success).toBe(false); + > 93 | +expect(result.statusCode).toBe(502); +  | ^ +  94 | expect(result.errorReason).toBe('HTTP 502'); +  95 | }); +  96 | + + at Object. (src/services/notification-failure-scenarios.test.ts:93:33) + + ● Notification Failure Scenarios (#793) › 2. Invalid Response › handles network-level disconnect or DNS resolution +failure + + expect(received).toContain(expected) // indexOf + + Expected substring: "ENOTFOUND" + Received string: "DNS resolution returned no addresses for hostname: invalid-domain.local" + +   102 | +  103 | expect(result.success).toBe(false); + > 104 | +expect(result.errorReason).toContain('ENOTFOUND'); +  | ^ +  105 | expect(result.statusCode).toBeUndefined(); +  106 | }); +  107 | }); + + at Object. (src/services/notification-failure-scenarios.test.ts:104:34) + + ● Notification Failure Scenarios (#793) › 3. Temporary Provider Failure › identifies 5xx as temporary failure +eligible for retry in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   123 | +  124 | expect(result.success).toBe(false); + > 125 | +expect(result.statusCode).toBe(503); +  | ^ +  126 | }); +  127 | +  128 | it('schedules next retry with backoff and keeps notification in PENDING +state', async () => { + + at Object. (src/services/notification-failure-scenarios.test.ts:125:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › classifies HTTP 404/401 client errors as +permanent failure in WebhookDeliveryService + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   208 | +  209 | expect(result.success).toBe(false); + > 210 | +expect(result.statusCode).toBe(404); +  | ^ +  211 | expect(result.errorReason).toBe('HTTP 404'); +  212 | }); +  213 | + + at Object. (src/services/notification-failure-scenarios.test.ts:210:33) + + ● Notification Failure Scenarios (#793) › 4. Permanent Provider Failure › marks notification failed permanently and +routes to dead letter queue + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   234 | const [sql, params] = (mockDb.run as +any).mock.calls[0]; +  235 | expect(sql).toContain('status = ?'); + > 236 | expect((params as +any[])[0]).toBe(NotificationStatus.FAILED); +  | ^ +  237 | expect((params as any[])[4]).toBeNull(); // +next_retry_at is null +  238 | +  239 | // Verify Dead-Letter Queue entry was created + + at Object. (src/services/notification-failure-scenarios.test.ts:236:36) + +FAIL src/__tests__/stress.test.ts (195.991 s) + ● Stress Test Suite: Notification Processing › 7. Error Recovery Under Stress › should gracefully handle failures +and retry under load + + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 180000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   638 | +  639 | describe('7. Error Recovery Under Stress', () => { + > 640 | it('should gracefully handle failures and retry under +load', async () => { +  | ^ +  641 | const eventCount = 1000; +  642 | const processedIds: string[] = []; +  643 | const latencies: number[] = []; + + at src/__tests__/stress.test.ts:640:5 + at src/__tests__/stress.test.ts:639:3 + at Object. (src/__tests__/stress.test.ts:38:1) + +FAIL src/services/concurrent-event-processing.test.ts (22.51 s) + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim when 50 tasks race +for the same event + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 50 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   155 | ); +  156 | + > 157 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  158 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  159 | expect(rows).toHaveLength(1); +  160 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:157:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › grants exactly one claim across two listener +instances (separate connections) + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 40 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, …] + +   172 | ); +  173 | + > 174 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  175 | const rows = await dbA.all('SELECT * FROM +processed_events'); +  176 | expect(rows).toHaveLength(1); +  177 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:174:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › claims every distinct event exactly once +under concurrency + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 200 + + Array [ + "evt-0", + + "evt-0", + + "evt-0", + + "evt-1", + + "evt-1", + "evt-1", + + "evt-10", + + "evt-10", + "evt-10", + + "evt-11", + "evt-11", + + "evt-11", + + "evt-12", + "evt-12", + + "evt-12", + + "evt-13", + + "evt-13", + "evt-13", + "evt-14", + + "evt-14", + + "evt-14", + + "evt-15", + + "evt-15", + "evt-15", + + "evt-16", + "evt-16", + + "evt-16", + + "evt-17", + + "evt-17", + "evt-17", + "evt-18", + + "evt-18", + + "evt-18", + "evt-19", + + "evt-19", + + "evt-19", + + "evt-2", + + "evt-2", + "evt-2", + "evt-20", + + "evt-20", + + "evt-20", + + "evt-21", + + "evt-21", + "evt-21", + + "evt-22", + + "evt-22", + "evt-22", + + "evt-23", + "evt-23", + + "evt-23", + + "evt-24", + "evt-24", + + "evt-24", + + "evt-25", + + "evt-25", + "evt-25", + + "evt-26", + "evt-26", + + "evt-26", + + "evt-27", + "evt-27", + + "evt-27", + + "evt-28", + + "evt-28", + "evt-28", + "evt-29", + + "evt-29", + + "evt-29", + + "evt-3", + "evt-3", + + "evt-3", + + "evt-30", + + "evt-30", + "evt-30", + + "evt-31", + "evt-31", + + "evt-31", + + "evt-32", + + "evt-32", + "evt-32", + + "evt-33", + "evt-33", + + "evt-33", + + "evt-34", + + "evt-34", + "evt-34", + "evt-35", + + "evt-35", + + "evt-35", + "evt-36", + + "evt-36", + + "evt-36", + + "evt-37", + + "evt-37", + "evt-37", + "evt-38", + + "evt-38", + + "evt-38", + "evt-39", + + "evt-39", + + "evt-39", + + "evt-4", + "evt-4", + + "evt-4", + "evt-40", + + "evt-40", + + "evt-40", + "evt-41", + + "evt-41", + + "evt-41", + + "evt-42", + "evt-42", + + "evt-42", + "evt-43", + + "evt-43", + + "evt-43", + + "evt-44", + + "evt-44", + "evt-44", + + "evt-45", + + "evt-45", + "evt-45", + + "evt-46", + "evt-46", + + "evt-46", + + "evt-47", + "evt-47", + + "evt-47", + + "evt-48", + + "evt-48", + "evt-48", + + "evt-49", + "evt-49", + + "evt-49", + + "evt-5", + "evt-5", + + "evt-5", + + "evt-50", + + "evt-50", + "evt-50", + "evt-51", + + "evt-51", + + "evt-51", + + "evt-52", + "evt-52", + + "evt-52", + + "evt-53", + + "evt-53", + "evt-53", + + "evt-54", + "evt-54", + + "evt-54", + + "evt-55", + + "evt-55", + "evt-55", + + "evt-56", + "evt-56", + + "evt-56", + + "evt-57", + + "evt-57", + "evt-57", + "evt-58", + + "evt-58", + + "evt-58", + "evt-59", + + "evt-59", + + "evt-59", + + "evt-6", + + "evt-6", + "evt-6", + "evt-60", + + "evt-60", + + "evt-60", + "evt-61", + + "evt-61", + + "evt-61", + + "evt-62", + "evt-62", + + "evt-62", + "evt-63", + + "evt-63", + + "evt-63", + "evt-64", + + "evt-64", + + "evt-64", + + "evt-65", + "evt-65", + + "evt-65", + "evt-66", + + "evt-66", + + "evt-66", + + "evt-67", + + "evt-67", + "evt-67", + + "evt-68", + + "evt-68", + "evt-68", + + "evt-69", + "evt-69", + + "evt-69", + + "evt-7", + "evt-7", + + "evt-7", + + "evt-70", + + "evt-70", + "evt-70", + + "evt-71", + "evt-71", + + "evt-71", + + "evt-72", + "evt-72", + + "evt-72", + + "evt-73", + + "evt-73", + "evt-73", + "evt-74", + + "evt-74", + + "evt-74", + + "evt-75", + "evt-75", + + "evt-75", + + "evt-76", + + "evt-76", + "evt-76", + "evt-77", + + "evt-77", + + "evt-77", + + "evt-78", + + "evt-78", + "evt-78", + + "evt-79", + "evt-79", + + "evt-79", + + "evt-8", + + "evt-8", + "evt-8", + "evt-80", + + "evt-80", + + "evt-80", + "evt-81", + + "evt-81", + + "evt-81", + + "evt-82", + + "evt-82", + "evt-82", + "evt-83", + + "evt-83", + + "evt-83", + "evt-84", + + "evt-84", + + "evt-84", + + "evt-85", + "evt-85", + + "evt-85", + "evt-86", + + "evt-86", + + "evt-86", + "evt-87", + + "evt-87", + + "evt-87", + + "evt-88", + "evt-88", + + "evt-88", + "evt-89", + + "evt-89", + + "evt-89", + + "evt-9", + + "evt-9", + "evt-9", + + "evt-90", + + "evt-90", + "evt-90", + + "evt-91", + "evt-91", + + "evt-91", + + "evt-92", + "evt-92", + + "evt-92", + + "evt-93", + + "evt-93", + "evt-93", + + "evt-94", + "evt-94", + + "evt-94", + + "evt-95", + "evt-95", + + "evt-95", + + "evt-96", + + "evt-96", + "evt-96", + "evt-97", + + "evt-97", + + "evt-97", + + "evt-98", + "evt-98", + + "evt-98", + + "evt-99", + + "evt-99", + "evt-99", + ] + +   190 | +  191 | const winners = results.filter((r) => +r.claimed).map((r) => r.id); + > 192 | +expect(winners.sort()).toEqual([...ids].sort()); +  | ^ +  193 | const count = await dbA.get<{ +n: number }>('SELECT COUNT(*) AS n FROM processed_events'); +  194 | expect(count?.n).toBe(100); +  195 | }); + + at Object. (src/services/concurrent-event-processing.test.ts:192:30) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not let redetections overwrite the final +outcome of a completed event + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   204 | await jitter(); +  205 | const claim = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 206 | +expect(claim.claimed).toBe(false); +  | ^ +  207 | await service.recordRedetection('evt-1', +CONTRACT, 100); +  208 | }), +  209 | ); + + at src/services/concurrent-event-processing.test.ts:206:33 + at async Promise.all (index 0) + at Object. (src/services/concurrent-event-processing.test.ts:202:7) + + ● concurrent event processing › EventDeduplicationService.claimEvent › does not count in-flight duplicates as reorg +redetections + + expect(received).toBe(expected) // Object.is equality + + Expected: "PROCESSING" + Received: undefined + +   221 | +  222 | const row = await +dbA.get<ProcessedRow>('SELECT * FROM processed_events WHERE +event_id = ?', ['evt-1']); + > 223 | +expect(row?.status).toBe('PROCESSING'); +  | ^ +  224 | +expect(row?.reorg_detection_count).toBe(0); +  225 | }); +  226 | + + at Object. (src/services/concurrent-event-processing.test.ts:223:27) + + ● concurrent event processing › EventDeduplicationService.claimEvent › lets exactly one worker take over a claim +abandoned past its lease + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 10 + Received array: [{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, +{"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": +true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, +"claimed": true}, {"alreadySeen": false, "claimed": true}, {"alreadySeen": false, "claimed": true}] + +   236 | Array.from({ length: 10 +}, () => recovery.claimEvent('evt-1', CONTRACT, +100, 'tx', 'contract')), +  237 | ); + > 238 | expect(results.filter((r) => +r.claimed)).toHaveLength(1); +  | ^ +  239 | }); +  240 | +  241 | it('honours an active claim within its lease', async () +=> { + + at Object. (src/services/concurrent-event-processing.test.ts:238:48) + + ● concurrent event processing › EventDeduplicationService.claimEvent › honours an active claim within its lease + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + +   243 | await service.claimEvent('evt-1', +CONTRACT, 100, 'tx', 'contract'); +  244 | const second = await +service.claimEvent('evt-1', CONTRACT, 100, +'tx', 'contract'); + > 245 | +expect(second.claimed).toBe(false); +  | ^ +  246 | }); +  247 | }); +  248 | + + at Object. (src/services/concurrent-event-processing.test.ts:245:30) + + ● concurrent event processing › EventSubscriber end to end › two listener instances polling the same events notify +each event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:322:13) + + ● concurrent event processing › EventSubscriber end to end › overlapping poll cycles in one instance notify each +event exactly once + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:330:13) + + ● concurrent event processing › EventSubscriber end to end › re-polling already-processed events sends nothing and +keeps outcomes intact + + expect(received).toHaveLength(expected) + + Expected length: 25 + Received length: 0 + Received array: [] + +   299 | // Exactly one notification per event. +  300 | const sentIds = +mockSendEventNotification.mock.calls.map((c) => +c[0].id); + > 301 | expect(sentIds).toHaveLength(EVENT_COUNT); +  | ^ +  302 | expect(new +Set(sentIds).size).toBe(EVENT_COUNT); +  303 | +  304 | // One row per event, all finalised, none stuck in PROCESSING. + + at assertConsistentState (src/services/concurrent-event-processing.test.ts:301:23) + at Object. (src/services/concurrent-event-processing.test.ts:336:13) + +FAIL src/services/execution-metrics.test.ts (10.666 s) + ● Execution Metrics Deduplication › should correctly count multiple notifications with different retry patterns + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   189 | +  190 | // Verify proper deduplication + > 191 | +expect(metrics.totalNotifications).toBe(3); +  | ^ +  192 | expect(metrics.successfulFirstAttempt).toBe(1); +// notif1 +  193 | expect(metrics.successfulAfterRetry).toBe(1); +// notif2 +  194 | expect(metrics.permanentFailures).toBe(1); // +notif3 + + at Object. (src/services/execution-metrics.test.ts:191:40) + + ● Execution Metrics Deduplication › should return retry distribution breakdown + + expect(received).toHaveLength(expected) + + Expected length: 3 + Received length: 2 + Received array: [{"failureCount": 0, "retryCount": 0, "successCount": 2}, {"failureCount": 0, "retryCount": 1, +"successCount": 3}] + +   288 | +  289 | // Verify distribution + > 290 | expect(distribution).toHaveLength(3); +  | ^ +  291 | +  292 | const retries0 = distribution.find((d) => +d.retryCount === 0); +  293 | expect(retries0?.successCount).toBe(2); + + at Object. (src/services/execution-metrics.test.ts:290:26) + + ● Execution Metrics Deduplication › should calculate accurate average durations + + expect(received).toBe(expected) // Object.is equality + + Expected: 2000 + Received: 0 + +   363 | +  364 | // Average failure duration: 2000ms + > 365 | +expect(metrics.averageFailureDurationMs).toBe(2000); +  | ^ +  366 | }); +  367 | +  368 | it('should handle empty database gracefully', async () +=> { + + at Object. (src/services/execution-metrics.test.ts:365:46) + +FAIL src/services/event-subscriber-reorg.test.ts + ● EventSubscriber with EventDeduplicationService - Reorg Scenarios › Reorg detection and handling › detects and +marks reorg duplicates + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Reorg duplicate detected", Anything + Received + 1: "Blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "newLedger": 100, "previousLedger": 102} + 2: "Potential blockchain reorg detected", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "eventLedger": 100, "requestId": "4d99eb21"} + 3: "Skipping event: already processed (persistent deduplication)", {"contractAddress": +"CCEMX6Q5V5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5F5", "correlationId": "ea0aaea1-ea79-4b33-8f3f-94d4ba69a2ff", +"eventId": "event-1", "requestId": "ea0aaea1-ea79-4b33-8f3f-94d4ba69a2ff"} + + Number of calls: 4 + +   217 | +  218 | // Warnings should be logged + > 219 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  220 | 'Reorg duplicate detected', +  221 | expect.anything() +  222 | ); + + at Object. (src/services/event-subscriber-reorg.test.ts:219:31) + +FAIL src/api/queue-operational-metrics.test.ts (15.625 s) + ● Notification Queue Operational Metrics (#797) › Repository & Service Layer Metrics › correctly tracks failed +deliveries and retry attempts + + expect(received).toBe(expected) // Object.is equality + + Expected: 1 + Received: 0 + +   225 | metrics = await +repository.getQueueOperationalMetrics(); +  226 | expect(metrics.pendingNotifications).toBe(0); + > 227 | +expect(metrics.failedDeliveries).toBe(1); +  | ^ +  228 | expect(metrics.retryAttempts).toBe(1); +  229 | }); +  230 | + + at Object. (src/api/queue-operational-metrics.test.ts:227:40) + +FAIL src/tests/template-system.test.ts + ● Notification Template System › TemplateRenderer › should render simple variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render multiple variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should render nested properties + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should handle missing variables gracefully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should escape HTML by default + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should throw in strict mode for missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should extract variables from template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should use default values + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateRenderer › should validate context + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate correct template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect unclosed brackets + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject empty variable + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should reject invalid variable name + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should detect script injection + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate unique key format + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateValidator › should validate channel-specific requirements + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should create template successfully + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject invalid template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should reject duplicate unique key + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should update template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should list templates with filters + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › TemplateService - CRUD Operations › should deactivate template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should render template via service + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should reject rendering with missing variables + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Template Rendering Integration › should log template usage + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent XSS via HTML escaping + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should reject script tags in template + + SQLITE_ERROR: no such column: unique_key + + ● Notification Template System › Security Tests › should prevent prototype pollution + + SQLITE_ERROR: no such column: unique_key + +FAIL src/services/webhook-delivery-service.test.ts + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 200 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   79 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  80 | + > 81 | +expect(result.success).toBe(true); +  | ^ +  82 | expect(result.statusCode).toBe(200); +  83 | expect(result.errorReason).toBeUndefined(); +  84 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:81:30) + + ● WebhookDeliveryService › successful delivery (2xx) › returns success: true for a 201 response + + expect(received).toBe(expected) // Object.is equality + + Expected: true + Received: false + +   89 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  90 | + > 91 | +expect(result.success).toBe(true); +  | ^ +  92 | expect(result.statusCode).toBe(201); +  93 | }); +  94 | + + at Object. (src/services/webhook-delivery-service.test.ts:91:30) + + ● WebhookDeliveryService › successful delivery (2xx) › logs an info message on successful delivery + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivered successfully", ObjectContaining {"requestId": "req-test-001", "statusCode": 200, +"targetUrl": "https://example.com/webhook"} + Received: "Delivering webhook", {"requestId": "req-test-001", "targetUrl": "https://example.com/webhook", +"timeoutMs": 5000} + + Number of calls: 1 + +   98 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  99 | + > 100 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  101 | 'Webhook delivered successfully', +  102 | expect.objectContaining({ requestId: REQUEST_ID, +targetUrl: TARGET_URL, statusCode: 200 }), +  103 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:100:27) + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 500 + + expect(received).toBe(expected) // Object.is equality + + Expected: 500 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 502 + + expect(received).toBe(expected) // Object.is equality + + Expected: 502 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 503 + + expect(received).toBe(expected) // Object.is equality + + Expected: 503 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › returns success: false for HTTP 504 + + expect(received).toBe(expected) // Object.is equality + + Expected: 504 + Received: undefined + +   128 | +  129 | expect(result.success).toBe(false); + > 130 | expect(result.statusCode).toBe(status); +  | ^ +  131 | expect(result.errorReason).toBe(`HTTP +${status}`); +  132 | }, +  133 | ); + + at src/services/webhook-delivery-service.test.ts:130:35 + + ● WebhookDeliveryService › 5xx server errors (retryable) › logs a warn (not error) for 5xx so the caller knows to +retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with server error (5xx) — will retry", ObjectContaining {"statusCode": 503} + + Number of calls: 0 + +   138 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  139 | + > 140 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  141 | 'Webhook delivery failed with server error (5xx) — will retry', +  142 | expect.objectContaining({ statusCode: 503 }), +  143 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:140:27) + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 400 + + expect(received).toBe(expected) // Object.is equality + + Expected: 400 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 401 + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 403 + + expect(received).toBe(expected) // Object.is equality + + Expected: 403 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 404 + + expect(received).toBe(expected) // Object.is equality + + Expected: 404 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › returns success: false for HTTP 422 + + expect(received).toBe(expected) // Object.is equality + + Expected: 422 + Received: undefined + +   157 | +  158 | expect(result.success).toBe(false); + > 159 | expect(result.statusCode).toBe(status); +  | ^ +  160 | expect(result.errorReason).toBe(`HTTP +${status}`); +  161 | }, +  162 | ); + + at src/services/webhook-delivery-service.test.ts:159:35 + + ● WebhookDeliveryService › 4xx client errors (permanent failure) › logs an error (not warn) for 4xx to signal +permanent failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Webhook delivery failed with client error (4xx) — no retry", ObjectContaining {"statusCode": 404} + Received: "Webhook delivery failed with network error — will retry", {"durationMs": 0, "error": "Cannot read +properties of undefined (reading 'get')", "requestId": "req-test-001", "targetUrl": "https://example.com/webhook"} + + Number of calls: 1 + +   167 | await service.deliver(TARGET_URL, +PAYLOAD, REQUEST_ID); +  168 | + > 169 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  170 | 'Webhook delivery failed with client error (4xx) — no retry', +  171 | expect.objectContaining({ statusCode: 404 }), +  172 | ); + + at Object. (src/services/webhook-delivery-service.test.ts:169:28) + + ● WebhookDeliveryService › failure classification › labels a 5xx response as "http_retryable" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_retryable" + Received: "network" + +   309 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  310 | + > 311 | +expect(result.failureReason).toBe('http_retryable'); +  | ^ +  312 | }); +  313 | +  314 | it('labels a 4xx response as "http_permanent"', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:311:36) + + ● WebhookDeliveryService › failure classification › labels a 4xx response as "http_permanent" + + expect(received).toBe(expected) // Object.is equality + + Expected: "http_permanent" + Received: "network" + +   317 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  318 | + > 319 | +expect(result.failureReason).toBe('http_permanent'); +  | ^ +  320 | }); +  321 | +  322 | it('leaves failureReason undefined on success', async () +=> { + + at Object. (src/services/webhook-delivery-service.test.ts:319:36) + + ● WebhookDeliveryService › failure classification › leaves failureReason undefined on success + + expect(received).toBeUndefined() + + Received: "network" + +   325 | const result = await +service.deliver(TARGET_URL, PAYLOAD, REQUEST_ID); +  326 | + > 327 | +expect(result.failureReason).toBeUndefined(); +  | ^ +  328 | }); +  329 | }); +  330 | }); + + at Object. (src/services/webhook-delivery-service.test.ts:327:36) + +FAIL src/__tests__/multi-channel-delivery.e2e.test.ts (34.093 s) + ● Multi-channel notification delivery (e2e) › partial delivery failure › delivers to healthy channels even when one +channel fails + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   225 | +  226 | describe('partial delivery failure', () => { + > 227 | it('delivers to healthy channels even when one channel +fails', async () => { +  | ^ +  228 | +router.setHandler(channels[1].config.webhookUrl, () => +  229 | failedFetch(500, 'Internal Server Error') +  230 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:227:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error identifying the +failing channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   235 | }); +  236 | + > 237 | it('logs a meaningful error identifying the failing +channel', async () => { +  | ^ +  238 | +router.setHandler(channels[1].config.webhookUrl, () => +  239 | failedFetch(429, 'Too Many Requests', +'rate limited') +  240 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:237:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › partial delivery failure › logs a meaningful error when a channel +throws a network error + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   260 | }); +  261 | + > 262 | it('logs a meaningful error when a channel throws a network +error', async () => { +  | ^ +  263 | +router.setHandler(channels[2].config.webhookUrl, () => { +  264 | throw new Error('ECONNRESET'); +  265 | }); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:262:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:226:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › retries only the failed channel and recovers without +touching healthy ones + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   289 | }; +  290 | + > 291 | it('retries only the failed channel and recovers without +touching healthy ones', async () => { +  | ^ +  292 | // 'ops' fails its first (immediate) attempt, then succeeds on retry. +  293 | +router.setHandler(channels[1].config.webhookUrl, (attempt) +=> +  294 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} + + at src/__tests__/multi-channel-delivery.e2e.test.ts:291:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › escalates to a permanent-failure log after +exhausting retries on a channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   315 | }); +  316 | + > 317 | it('escalates to a permanent-failure log after exhausting +retries on a channel', async () => { +  | ^ +  318 | // 'audit' never recovers. +  319 | +router.setHandler(channels[2].config.webhookUrl, () => +failedFetch(500, 'Internal Server Error')); +  320 | + + at src/__tests__/multi-channel-delivery.e2e.test.ts:317:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + + ● Multi-channel notification delivery (e2e) › retry behaviour › keeps retry queues independent per channel + + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a test. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   340 | }); +  341 | + > 342 | it('keeps retry queues independent per channel', +async () => { +  | ^ +  343 | +router.setHandler(channels[0].config.webhookUrl, (attempt) +=> +  344 | attempt === 0 ? failedFetch(503, +'Service Unavailable') : { ok: true, status: 204 +} +  345 | ); + + at src/__tests__/multi-channel-delivery.e2e.test.ts:342:5 + at src/__tests__/multi-channel-delivery.e2e.test.ts:277:3 + at Object. (src/__tests__/multi-channel-delivery.e2e.test.ts:114:1) + +FAIL src/services/notification-retry-queue.test.ts + ● NotificationRetryQueue › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   110 | await Promise.resolve(); +  111 | await Promise.resolve(); + > 112 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  113 | +  114 | queue.stop(); +  115 | }); + + at Object. (src/services/notification-retry-queue.test.ts:112:30) + + ● NotificationRetryQueue › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   129 | await Promise.resolve(); +  130 | + > 131 | expect(queue.size()).toBe(0); +  | ^ +  132 | queue.stop(); +  133 | }); +  134 | + + at Object. (src/services/notification-retry-queue.test.ts:131:28) + + ● NotificationRetryQueue › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": "CA123", "correlationId": "req-ok", "delayMs": +3817.8322590651774, "eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:42:17.925Z", "priority": +"Medium", "requestId": "req-ok"} + + Number of calls: 1 + +   147 | await Promise.resolve(); +  148 | + > 149 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  150 | 'Retry succeeded', +  151 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  152 | ); + + at Object. (src/services/notification-retry-queue.test.ts:149:27) + + ● NotificationRetryQueue › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   171 | await Promise.resolve(); +  172 | await Promise.resolve(); + > 173 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  174 | +  175 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  176 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue.test.ts:173:30) + + ● NotificationRetryQueue › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   199 | await Promise.resolve(); +  200 | + > 201 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  202 | 'Retry failed, scheduling next attempt', +  203 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  204 | ); + + at Object. (src/services/notification-retry-queue.test.ts:201:27) + + ● NotificationRetryQueue › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   226 | jest.advanceTimersByTime(100); +  227 | await flush(); + > 228 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  229 | +  230 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  231 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue.test.ts:228:30) + + ● NotificationRetryQueue › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   258 | await Promise.resolve(); +  259 | + > 260 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  261 | 'Notification permanently failed after max retries', +  262 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  263 | ); + + at Object. (src/services/notification-retry-queue.test.ts:260:28) + + ● NotificationRetryQueue › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue.test.ts:300:30) + + ● NotificationRetryQueue › analytics success counter (regression: no duplicate-counting) › increments success +exactly once when an operation succeeds after multiple failed retries + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   350 | +  351 | // The notification fn was called exactly 3 times + > 352 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  353 | +  354 | // Success must be exactly 1 — not 0 (missing) and not >1 (duplicate) +  355 | expect(snap.overall.success).toBe(1); + + at Object. (src/services/notification-retry-queue.test.ts:352:30) + +FAIL src/services/scheduled-notification-queue.test.ts (14.471 s) + ● Durable notification queue › retains the terminal processing state of a permanently failed job + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   278 | +  279 | const failed = await +repository.getById(id); + > 280 | expect(failed!.status).toBe(Notificatio +nStatus.FAILED); +  | ^ +  281 | expect(failed!.retryCount).toBe(2); +  282 | expect(failed!.lastError).toBe('permanent +outage'); +  283 | expect(failed!.errorDetails).toContain('permanent +outage'); + + at Object. (src/services/scheduled-notification-queue.test.ts:280:28) + +FAIL src/utils/log-redaction.test.ts + ● events server logging paths › webhook signature failures never log the secret or signature + + expect(received).toBe(expected) // Object.is equality + + Expected: 401 + Received: 400 + +   251 | }); +  252 | + > 253 | expect(status).toBe(401); +  | ^ +  254 | expect(output()).toContain('Webhook'); +  255 | expectNoSecrets(); +  256 | expect(output()).not.toContain(forged.slice('sha256=' +.length)); + + at Object. (src/utils/log-redaction.test.ts:253:20) + +FAIL src/services/event-subscriber-backfill.test.ts + ● EventSubscriber — backfill safety limit (#628) › RPC error during tip resolution › falls back to ledger 1 and logs +a WARN when getLatestLedger fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Failed to fetch network tip for backfill limit; falling back to ledger 1", ObjectContaining +{"backfillMaxLedgers": 5000, "error": "RPC timeout"} + Received: "Failed to fetch network tip for backfill limit; falling back to ledger 1", {"backfillMaxLedgers": 5000, +"error": "Stellar RPC operation \"getLatestLedger\" failed across 1 endpoint(s): +[https://soroban-testnet.stellar.org:443]: RPC timeout"} + + Number of calls: 1 + +   245 | ); +  246 | + > 247 | expect(mockLogger.warn).toHaveBeenCalledWith( +  | ^ +  248 | 'Failed to fetch network tip for backfill limit; falling back to ledger +1', +  249 | expect.objectContaining({ +  250 | error: 'RPC timeout', + + at Object. (src/services/event-subscriber-backfill.test.ts:247:31) + +FAIL src/services/notification-retry-queue-refactored.test.ts + ● NotificationRetryQueue (Refactored) › processQueue › retries a notification after the base delay + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   87 | await Promise.resolve(); +  88 | await Promise.resolve(); + > 89 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  90 | +  91 | queue.stop(); +  92 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:89:30) + + ● NotificationRetryQueue (Refactored) › processQueue › removes the item from the queue on success + + expect(received).toBe(expected) // Object.is equality + + Expected: 0 + Received: 1 + +   107 | await Promise.resolve(); +  108 | + > 109 | expect(queue.size()).toBe(0); +  | ^ +  110 | queue.stop(); +  111 | }); +  112 | + + at Object. (src/services/notification-retry-queue-refactored.test.ts:109:28) + + ● NotificationRetryQueue (Refactored) › processQueue › logs success on a successful retry + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry succeeded", ObjectContaining {"attempt": 1, "eventId": "evt-ok"} + Received: "Notification queued for retry", {"contractAddress": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "correlationId": "req-ok", "delayMs": 6018.786605702059, +"eventId": "evt-ok", "maxRetries": 5, "nextRetryAt": "2026-10-16T12:42:36.357Z", "priority": "Medium", "requestId": +"req-ok"} + + Number of calls: 1 + +   132 | await Promise.resolve(); +  133 | + > 134 | expect(logger.info).toHaveBeenCalledWith( +  | ^ +  135 | 'Retry succeeded', +  136 | expect.objectContaining({ eventId: 'evt-ok', +attempt: 1 }) +  137 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:134:27) + + ● NotificationRetryQueue (Refactored) › exponential backoff › doubles the delay on each successive failure + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   157 | await Promise.resolve(); +  158 | await Promise.resolve(); + > 159 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  160 | +  161 | // Trigger attempt 2 (after 2000 ms from attempt 1) +  162 | jest.advanceTimersByTime(2100); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:159:30) + + ● NotificationRetryQueue (Refactored) › exponential backoff › logs a warning with the next retry delay on failure + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Retry failed, scheduling next attempt", ObjectContaining {"attempt": 1, "delayMs": 2000, "eventId": +"evt-backoff"} + + Number of calls: 0 + +   190 | await Promise.resolve(); +  191 | + > 192 | expect(logger.warn).toHaveBeenCalledWith( +  | ^ +  193 | 'Retry failed, scheduling next attempt', +  194 | expect.objectContaining({ eventId: +'evt-backoff', attempt: 1, delayMs: 2000 }) +  195 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:192:27) + + ● NotificationRetryQueue (Refactored) › max retries › stops retrying after maxRetries attempts + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   219 | jest.advanceTimersByTime(100); +  220 | await flush(); + > 221 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  222 | +  223 | // attempt 2 fires at t=300ms (100 + 100*2^1 = 300) +  224 | jest.advanceTimersByTime(200); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:221:30) + + ● NotificationRetryQueue (Refactored) › max retries › logs an error when the notification permanently fails + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"eventId": "evt-dead", +"totalAttempts": 1} + + Number of calls: 0 + +   256 | await Promise.resolve(); +  257 | + > 258 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  259 | 'Notification permanently failed after max retries', +  260 | expect.objectContaining({ eventId: 'evt-dead', +totalAttempts: 1 }) +  261 | ); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:258:28) + + ● NotificationRetryQueue (Refactored) › start / stop › calling start twice does not double-process items + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   298 | await Promise.resolve(); +  299 | + > 300 | +expect(notificationFn).toHaveBeenCalledTimes(1); +  | ^ +  301 | queue.stop(); +  302 | }); +  303 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:300:30) + + ● NotificationRetryQueue (Refactored) › multiple events with different characteristics › processes events with +different topics + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 0 + +   327 | } +  328 | + > 329 | +expect(notificationFn).toHaveBeenCalledTimes(3); +  | ^ +  330 | queue.stop(); +  331 | }); +  332 | }); + + at Object. (src/services/notification-retry-queue-refactored.test.ts:329:30) + +FAIL src/api/execution-metrics-api.test.ts (12.289 s) + ● Execution Metrics API Integration › GET /api/schedule/retry-distribution should return retry breakdown + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 2 + +   223 | // Verify distribution structure +  224 | +expect(Array.isArray(distribution)).toBe(true); + > 225 | +expect(distribution.length).toBe(3); +  | ^ +  226 | +  227 | // Check each retry level +  228 | const retries0 = distribution.find((d: any) +=> d.retryCount === 0); + + at Object. (src/api/execution-metrics-api.test.ts:225:33) + +FAIL src/services/notification-claim-lease.test.ts + ● NotificationClaimLease › serialises renewals so a slow heartbeat cannot overlap itself + + expect(received).toBe(expected) // Object.is equality + + Expected: 3 + Received: 1 + +   161 | +  162 | expect(maxInFlight).toBe(1); + > 163 | +expect(lease.getRenewalCount()).toBe(3); +  | ^ +  164 | }); +  165 | +  166 | it('marks the lease lost and stops renewing when the claim is gone', +async () => { + + at Object. (src/services/notification-claim-lease.test.ts:163:37) + +FAIL src/api/template-routes.test.ts + ● template-routes validation › POST /api/templates › rejects a request missing required fields without calling the +service + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:85:7) + + ● template-routes validation › POST /api/templates › rejects an invalid channelType + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:107:7) + + ● template-routes validation › POST /api/templates › rejects malformed JSON with a 400, not a 500 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:75:5) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:117:7) + + ● template-routes validation › POST /api/templates › returns 400 when the service reports the template failed +validation + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:137:7) + + ● template-routes validation › POST /api/templates › returns 201 when the service accepts the template + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleCreateTemplate (src/api/template-routes.ts:163:5) + at handleTemplateRoutes (src/api/template-routes.ts:432:5) + at Object. (src/api/template-routes.test.ts:153:7) + + ● template-routes validation › GET /api/templates › rejects an invalid channelType query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:69:5) + at handleListTemplates (src/api/template-routes.ts:202:5) + at handleTemplateRoutes (src/api/template-routes.ts:438:11) + at Object. (src/api/template-routes.test.ts:165:33) + + ● template-routes validation › PUT /api/templates/:id › returns 404 when the service reports the template does not +exist + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:201:7) + + ● template-routes validation › PUT /api/templates/:id › returns 400 (not 200) when the service rejects the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:215:7) + + ● template-routes validation › PUT /api/templates/:id › returns 200 when the service accepts the update + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleUpdateTemplate (src/api/template-routes.ts:296:5) + at handleTemplateRoutes (src/api/template-routes.ts:468:5) + at Object. (src/api/template-routes.test.ts:226:7) + + ● template-routes validation › POST /api/templates/render › rejects a non-object context + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:237:7) + + ● template-routes validation › POST /api/templates/render › rejects a request missing both templateId and uniqueKey + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:247:7) + + ● template-routes validation › POST /api/templates/render › surfaces missing variables from the service as a 400 + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleRenderTemplate (src/api/template-routes.ts:379:5) + at handleTemplateRoutes (src/api/template-routes.ts:444:5) + at Object. (src/api/template-routes.test.ts:261:7) + + ● template-routes validation › GET /api/templates/stats › rejects a non-numeric templateId query param + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:11) + at Object. (src/api/template-routes.test.ts:273:33) + + ● template-routes validation › GET /api/templates/stats › falls back to overview stats when no templateId is given + + TypeError: res.setHeader is not a function + +   51 |  */ +  52 | function sendJson(res: http.ServerResponse, +statusCode: number, data: any): void { + > 53 | res.setHeader('Content-Type', +'application/json'); +  | ^ +  54 | res.writeHead(statusCode, { 'Content-Type': +'application/json' }); +  55 | res.end(JSON.stringify(data)); +  56 | } + + at sendJson (src/api/template-routes.ts:53:7) + at respondWithError (src/api/template-routes.ts:92:3) + at handleGetTemplateStats (src/api/template-routes.ts:412:5) + at handleTemplateRoutes (src/api/template-routes.ts:450:5) + at Object. (src/api/template-routes.test.ts:283:7) + +FAIL src/__tests__/config-startup-validation.test.ts + ● Startup Configuration Validation Integration › Invalid configuration prevents startup › prevents service +instantiation by throwing ConfigError during validation + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:52:32) + + ● Startup Configuration Validation Integration › Multiple configuration errors reported together › collects and +reports all configuration errors in a single message + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:99:32) + + ● Startup Configuration Validation Integration › Validation prevents event processing › ensures invalid config is +caught before any service initialization + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:145:32) + + ● Startup Configuration Validation Integration › Specific listener configuration requirements › requires at least +one contract address to monitor + + ConfigError: CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. Add contract +configurations or the service will not process any events. + +   124 | +  125 | if (value.length === 0) { + > 126 | throw new ConfigError( +  | ^ +  127 | 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. +' + +  128 | 'Add contract configurations or the service will not process any events.' +  129 | ); + + at validateContractAddresses (src/config.ts:126:11) + at loadConfig (src/config.ts:446:24) + at Object. (src/__tests__/config-startup-validation.test.ts:169:32) + +FAIL src/__tests__/retry-scheduler.integration.test.ts (26.933 s) + ● RetryScheduler integration › marks notification FAILED when max retries exhausted + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   136 | ); +  137 | + > 138 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  139 | expect(row!.retry_count).toBe(3); +  140 | }); +  141 | + + at Object. (src/__tests__/retry-scheduler.integration.test.ts:138:25) + +FAIL src/tests/events-server.test.ts + ● Test suite failed to run + + Jest encountered an unexpected token + + Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript +syntax, or when Jest is not configured to support such syntax. + + Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel +configuration. + + By default "node_modules" folder is ignored by transformers. + + Here's what you can do: + • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript + • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in +your config. + • If you need a custom transformation, specify a "transform" option in your config. + • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the +"moduleNameMapper" config option. + + You'll find more details and examples of these config options in the docs: + https://jestjs.io/docs/configuration + For information about custom transformations, see: + https://jestjs.io/docs/code-transformation + + Details: + + C:\Users\NEW OWNER\Documents\drips\core foundry 941\Notify-Chain\listener\src\tests\events-server.test.ts:11 + headers ? : Record; + ^ + + SyntaxError: Unexpected token ':' + + at ModuleExecutor.compile (node_modules/jest-runtime/build/index.js:3081:44) + +FAIL src/services/notification-deduplicator.test.ts + ● generateFingerprint › combines contractAddress and eventId + + expect(received).toBe(expected) // Object.is equality + + Expected: "CABC123:event-42" + Received: "98b716c95d870cb93ddde27ed6f5936596f49c835b2a6f210311023114907b4a" + +   12 | it('combines contractAddress and eventId', () => { +  13 | const fp = generateFingerprint('event-42', +'CABC123'); + > 14 | expect(fp).toBe('CABC123:event-42'); +  | ^ +  15 | }); +  16 | +  17 | it('produces different fingerprints for different event ids', () +=> { + + at Object. (src/services/notification-deduplicator.test.ts:14:16) + + ● NotificationDeduplicator › time window and metrics › reports skipped duplicate metrics accurately + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 2 + + @@ -2,7 +2,9 @@ + "acceptedRequests": 2, + "cacheSize": 2, + "deduplicationWindowMs": 1000, + "evictedEntries": 0, + "expiredEntries": 0, + + "hitRatio": 66.67, + "skippedDuplicates": 2, + + "totalChecks": 3, + } + +   124 | d.isDuplicate('CONTRACT:event-new'); +  125 | + > 126 | expect(d.getMetrics()).toEqual({ +  | ^ +  127 | acceptedRequests: 2, +  128 | skippedDuplicates: 2, +  129 | evictedEntries: 0, + + at Object. (src/services/notification-deduplicator.test.ts:126:30) + +FAIL src/services/cleanup-service.test.ts + ● CleanupService › calling start twice does not create duplicate intervals + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 3 + Received number of calls: 4 + +   163 | jest.advanceTimersByTime(400); +  164 | // Should only fire once despite two start() calls + > 165 | +expect(db.run).toHaveBeenCalledTimes(3); // one interval tick × 3 SQL +statements +  | ^ +  166 | }); +  167 | }); +  168 | + + at Object. (src/services/cleanup-service.test.ts:165:20) + +FAIL src/api/archive-api.test.ts + ● archive-api validation › POST /api/archive/run › runs the archive cycle when the service is configured + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 3 + + Object { + + "data": Object { + "archived": 3, + + }, + + "success": true, + } + +   168 | +  169 | expect(res.statusCode).toBe(200); + > 170 | expect(res.body).toEqual({ archived: +3 }); +  | ^ +  171 | }); +  172 | }); +  173 | }); + + at Object. (src/api/archive-api.test.ts:170:24) + +FAIL src/database/event-notification-indexes.test.ts + ● event & notification query indexes (migration 003) › creates the new indexes on a fresh database via schema.sql + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › migration down() removes and up() restores the indexes, +idempotently + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › archival batch query uses the archivable index without a temp +sort + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › notification search by type uses the LOWER(notification_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › processed-event search by type uses the LOWER(event_type) +index + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + + ● event & notification query indexes (migration 003) › returns identical results with and without the new indexes + + SQLITE_CONSTRAINT: CHECK constraint failed: notification_type IN ('discord', 'email', 'webhook', 'sms') + +FAIL src/tests/payload-integrity.test.ts (7.21 s) + ● payload integrity — repository and scheduler integration › stored hash matches the payload + + expect(received).toBe(expected) // Object.is equality + + Expected: "c35f2d5840ea2ef0c330b3d278c0ee226b1619bedfb3968f9fb39ffc358a3dad" + Received: "c83ae106023bb061518e4f11773d72026b2caa4cef054d6f349c91ce7cbb4eb9" + +   115 | const notification = await +repository.getById(id); +  116 | const expected = +hashPayload(JSON.stringify(payload), SECRET); + > 117 | +expect(notification!.payloadHash).toBe(expected); +  | ^ +  118 | }); +  119 | +  120 | it('detects a tampered payload', async () => { + + at Object. (src/tests/payload-integrity.test.ts:117:39) + +FAIL src/services/notification-api-webhook.test.ts + ● NotificationAPI.scheduleWebhookNotification › forwards the payload to the repository + + expect(received).toEqual(expected) // deep equality + + - Expected - 0 + + Received + 1 + + Object { + "event": "order.shipped", + "orderId": "ord-001", + + "version": 1, + } + +   78 | +  79 | const [input] = mockCreate.mock.calls[0] +as any[]; + > 80 | +expect(input.payload).toEqual(PAYLOAD); +  | ^ +  81 | }); +  82 | +  83 | it('respects the maxRetries option', async () => { + + at Object. (src/services/notification-api-webhook.test.ts:80:27) + +FAIL src/tests/api-versioning.test.ts + ● API Route Versioning (#386) › /api/v1/events response has events array + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   80 | expect(status).toBe(200); +  81 | const json = JSON.parse(body); + > 82 | expect(json).toHaveProperty('events'); +  | ^ +  83 | +expect(Array.isArray(json.events)).toBe(true); +  84 | }); +  85 | + + at Object. (src/tests/api-versioning.test.ts:82:18) + + ● API Route Versioning (#386) › /api/v1/events?limit=5 honours the limit parameter + + expect(received).toHaveProperty(path) + + Expected path: "events" + Received path: [] + + Received value: {"data": {"count": 0, "events": []}, "success": true} + +   105 | expect(status).toBe(200); +  106 | const json = JSON.parse(body); + > 107 | expect(json).toHaveProperty('events'); +  | ^ +  108 | +expect(json.events.length).toBeLessThanOrEqual(5); +  109 | }); +  110 | + + at Object. (src/tests/api-versioning.test.ts:107:18) + +FAIL src/__tests__/query-performance.test.ts + ● Test suite failed to run + + Cannot find module 'vitest' from 'src/__tests__/query-performance.test.ts' + + > 1 | import { describe, it, expect } +from "vitest"; // if the repo uses jest, DELETE this line +  | ^ +  2 | import { readFileSync, writeFileSync, mkdirSync } from +"node:fs"; +  3 | import { dirname, resolve } from "node:path"; +  4 | import { performance } from "node:perf_hooks"; + + at Resolver._throwModNotFoundError (node_modules/jest-resolve/build/index.js:1031:11) + at Object. (src/__tests__/query-performance.test.ts:1:1) + +FAIL src/services/dead-letter-queue.test.ts (9.059 s) + ● Dead letter queue processing › moves exhausted notifications into the dead letter queue + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   10 | let repository: ScheduledNotificationRepository; +  11 | + > 12 | beforeEach(async () => { +  | ^ +  13 | if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); +  14 | const dbDir = path.dirname(dbPath); +  15 | if (!fs.existsSync(dbDir)) +fs.mkdirSync(dbDir, { recursive: true }); + + at src/services/dead-letter-queue.test.ts:12:3 + at Object. (src/services/dead-letter-queue.test.ts:7:1) + + ● Dead letter queue processing › requeues a dead-lettered notification for retry + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-dead-letter-queue.db' + +   22 | afterEach(async () => { +  23 | await db.close(); + > 24 | if (fs.existsSync(dbPath)) +fs.unlinkSync(dbPath); +  | ^ +  25 | }); +  26 | +  27 | it('moves exhausted notifications into the dead letter queue', +async () => { + + at Object. (src/services/dead-letter-queue.test.ts:24:35) + +FAIL src/database/query-performance.test.ts (12.076 s) + ● query performance indexes (migration 002) › creates the performance indexes from schema / migrations + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   22 | let dbPath: string; +  23 | + > 24 | beforeAll(async () => { +  | ^ +  25 | dbPath = path.join(os.tmpdir(), +`notify-perf-${Date.now()}.db`); +  26 | db = new Database(dbPath); +  27 | await db.initialize(); + + at src/database/query-performance.test.ts:24:3 + at Object. (src/database/query-performance.test.ts:20:1) + + ● query performance indexes (migration 002) › uses an indexed plan for pending claim queries + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   22 | let dbPath: string; +  23 | + > 24 | beforeAll(async () => { +  | ^ +  25 | dbPath = path.join(os.tmpdir(), +`notify-perf-${Date.now()}.db`); +  26 | db = new Database(dbPath); +  27 | await db.initialize(); + + at src/database/query-performance.test.ts:24:3 + at Object. (src/database/query-performance.test.ts:20:1) + +FAIL src/services/notification-metrics-store.test.ts (14.036 s) + ● NotificationMetricsStore › persists and retrieves summarized snapshots + + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + + at _getError (node_modules/jest-circus/build/jestAdapterInit.js:1985:12) + at Array.map () + + Cause: + thrown: "Exceeded timeout of 5000 ms for a hook. + Add a timeout value to this test to increase the timeout, if this is a long-running test. See +https://jestjs.io/docs/api#testname-fn-timeout." + +   21 | let store: NotificationMetricsStore; +  22 | + > 23 | beforeEach(async () => { +  | ^ +  24 | if (fs.existsSync(dbPath)) { +  25 | fs.unlinkSync(dbPath); +  26 | } + + at src/services/notification-metrics-store.test.ts:23:3 + at Object. (src/services/notification-metrics-store.test.ts:18:1) + + ● NotificationMetricsStore › purges snapshots older than the retention window + + EBUSY: resource busy or locked, unlink 'C:\Users\NEW OWNER\Documents\drips\core foundry +941\Notify-Chain\listener\data\test-metrics-store.db' + +   33 | await db.close(); +  34 | if (fs.existsSync(dbPath)) { + > 35 | fs.unlinkSync(dbPath); +  | ^ +  36 | } +  37 | }); +  38 | + + at Object. (src/services/notification-metrics-store.test.ts:35:10) + +FAIL src/api/request-duration-logger.test.ts (7.081 s) + ● API Request Duration Logging (Issue #687) › redacts sensitive secrets in query strings before logging + + TypeError: Cannot read properties of undefined (reading 'remoteAddress') + +   44 | durationMs, +  45 | requestId: (req.headers['x-request-id'] as +string) || (req as unknown as { id?: string +}).id, + > 46 | ip: req.ip || +req.socket.remoteAddress, +  | ^ +  47 | contentLength: res.getHeader('content-length') +  48 | ? +parseInt(String(res.getHeader('content-length')), 10) +  49 | : undefined, + + at EventEmitter. (src/api/request-duration-logger.ts:46:34) + at Object. (src/api/request-duration-logger.test.ts:71:16) + + +Test Suites: 52 failed, 1 skipped, 100 passed, 152 of 153 total +Tests: 272 failed, 4 skipped, 2247 passed, 2523 total +Snapshots: 0 total +Time: 535.835 s diff --git a/listener/_run3.txt b/listener/_run3.txt new file mode 100644 index 00000000..902669e5 --- /dev/null +++ b/listener/_run3.txt @@ -0,0 +1,874 @@ +node : FAIL src/tests/notification-scheduler-refactored.test.ts (33.046 s) +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=2 --wor ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/tests/...t.ts (33.046 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + ● Console + + console.log + 2026-10-16T12:51:42.227Z info: Connected to SQLite database +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:43.478Z error: Database run error {"sql":"ALTER TABLE scheduled_notifications ADD +COLUMN next_retry_at DATETIME","params":[],"error":"{\"errno\":1,\"code\":\"SQLITE_ERROR\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:43.483Z info: Database migrations completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:43.497Z info: Database initialized successfully +{"path":"./data/test-notifications-refactored.db"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:43.571Z info: Scheduled notification created {"id":1,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:43.948Z info: Scheduled notification created {"id":2,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:43.979Z info: Scheduled notification created {"id":3,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:44.184Z info: Fetched and locked pending notifications +{"count":2,"processorId":"test-processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:44.405Z info: Scheduled notification created {"id":4,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:44.485Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:44.581Z info: Scheduled notification created {"id":5,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:44.632Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:44.834Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:45.811Z info: Scheduled notification created {"id":6,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:46.190Z info: Notification marked as completed {"id":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:46.850Z info: Scheduled notification created {"id":7,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:46.917Z info: Notification marked for retry or failed +{"id":7,"newStatus":"PENDING","retryCount":1,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.017Z info: Scheduled notification created {"id":8,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.114Z warn: Notification moved to dead letter queue +{"id":8,"notificationType":"discord","failureReason":"Test error"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.132Z info: Notification marked for retry or failed +{"id":8,"newStatus":"DEAD_LETTERED","retryCount":4,"maxRetries":3} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.282Z info: Scheduled notification created {"id":9,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.317Z info: Notification cancelled {"id":9} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.487Z info: Scheduled notification created {"id":10,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.537Z info: Scheduled notification created {"id":11,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.818Z info: Scheduled notification created {"id":12,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:47.899Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.197Z warn: Recovered stale locks {"count":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.432Z info: Scheduled notification created {"id":13,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.485Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.530Z error: Database run error {"sql":"\n INSERT INTO +notification_execution_log (\n scheduled_notification_id, execution_attempt, status,\n error_message, +response_data, duration_ms\n ) VALUES (?, ?, ?, ?, ?, ?)\n ","params":[13,1,"DEAD_LETTERED","Lock +expired/Processor timeout",null,null],"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.540Z error: Transaction rolled back +{"error":"{\"errno\":19,\"code\":\"SQLITE_CONSTRAINT\"}"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.601Z info: Scheduled notification created {"id":14,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.635Z info: Scheduled notification created {"id":15,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.680Z info: Fetched and locked pending notifications +{"count":1,"processorId":"processor-1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.728Z info: Scheduled notification created {"id":16,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.789Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.925Z info: Scheduled notification created {"id":17,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:48.970Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.014Z info: Scheduled notification created {"id":18,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.062Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.088Z info: Scheduled notification created {"id":19,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.107Z info: Scheduling new notification +{"type":"email","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.135Z info: Scheduled notification created {"id":20,"executeAt":{},"type":"email"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.147Z info: Scheduling new notification +{"type":"webhook","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.185Z info: Scheduled notification created {"id":21,"executeAt":{},"type":"webhook"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.197Z info: Scheduling new notification +{"type":"sms","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.234Z info: Scheduled notification created {"id":22,"executeAt":{},"type":"sms"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.274Z info: Scheduling new notification +{"type":"discord","executeAt":{},"recipient":"https://discord.com/api/webhooks/[REDACTED]","version":1} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.303Z info: Scheduled notification created {"id":23,"executeAt":{},"type":"discord"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:49.320Z info: Database connection closed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● NotificationScheduler (Refactored) › ScheduledNotificationRepository › should mark as failed after max retries + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   182 | +  183 | const notification = await +repository.getById(id); + > 184 | expect(notification!.status).toBe(Not +ificationStatus.FAILED); +  | ^ +  185 | expect(notification!.retryCount).toBe(4); +  186 | }); +  187 | + + at Object. (src/tests/notification-scheduler-refactored.test.ts:184:36) + + ● NotificationScheduler (Refactored) › ScheduledNotificationRepository › should mark as failed if retry_count +reaches max_retries on lock recovery + + SQLITE_CONSTRAINT: CHECK constraint failed: status IN ('SUCCESS', 'FAILED', 'RETRY') + +FAIL src/__tests__/notification-flow-e2e.test.ts (45.888 s) + ● Console + + console.log + 2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:51.237Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:51.356Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:05:01.400Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:05:01.400Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:05:01.400Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:51.539Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:51.641Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:51.798Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: Initiating graceful shutdown {"activeJobs":0} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: No active jobs - proceeding with shutdown + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-06-24T12:00:00.250Z info: All jobs have completed - shutdown can proceed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:51.939Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:55.548Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:56.087Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:56.435Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:58.636Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:59.203Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:59.634Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:51:59.817Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:00.515Z info: Worker manager force shutdown completed + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● 1. Notification creation › rejects a duplicate idempotency key used with a different payload + + expect(received).rejects.toThrow(expected) + + Expected substring: "Idempotency key reused with different request body" + Received message: "Idempotency key reused with a different request body" + +   131 | if (!isValidRequest) { +  132 | logger.warn('Idempotency key reused with different request body +(replay suspected)', logMeta); + > 133 | throw new +IdempotencyKeyReuseError(); +  | ^ +  134 | } +  135 | +  136 | logger.info('Replay intercepted: returning cached idempotent +response', { + + at IdempotencyKeyService.processWithIdempotency (src/services/idempotency-key-service.ts:133:15) + at NotificationAPI.scheduleNotification (src/services/notification-api.ts:168:9) + at Object. (src/__tests__/notification-flow-e2e.test.ts:253:5) + at Object.toThrow (node_modules/expect/build/index.js:2155:20) + at Object. (src/__tests__/notification-flow-e2e.test.ts:259:15) + + ● 2. Delivery workflows › processes a due notification and transitions it to COMPLETED + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   343 | +  344 | const row = await repository.getById(id); + > 345 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  346 | expect(sendMock).toHaveBeenCalledTimes(1); +  347 | }); +  348 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:345:25) + + ● 2. Delivery workflows › delivers once executeAt elapses mid-run + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   377 | await jest.advanceTimersByTimeAsync(400); +  378 | + > 379 | expect((await repository.getById(id))!. +status).toBe(NotificationStatus.COMPLETED); +  | ^ +  380 | expect(sendMock).toHaveBeenCalledTimes(1); +  381 | }); +  382 | + + at Object. (src/__tests__/notification-flow-e2e.test.ts:379:52) + + ● 2. Delivery workflows › writes a SUCCESS execution log entry after delivery + + expect(received).toHaveLength(expected) + + Expected length: 1 + Received length: 0 + Received array: [] + +   396 | [id] +  397 | ); + > 398 | expect(logs).toHaveLength(1); +  | ^ +  399 | expect(logs[0].status).toBe('SUCCESS'); +  400 | expect(logs[0].execution_attempt).toBe(1); +  401 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:398:18) + + ● 2. Delivery workflows › processes a batch of multiple due notifications + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   418 | +  419 | for (const id of ids) { + > 420 | expect((await repository.getById(id))![33 +m.status).toBe(NotificationStatus.COMPLETED); +  | ^ +  421 | } +  422 | expect(sendMock).toHaveBeenCalledTimes(3); +  423 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:420:54) + + ● 2. Delivery workflows › forwards the event and contract config to the Discord service + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: ObjectContaining {"id": "deliver-evt-1"}, ObjectContaining {"address": +"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"}, Any + + Number of calls: 0 + +   438 | await jest.advanceTimersByTimeAsync(250); +  439 | + > 440 | expect(sendMock).toHaveBeenCalledWith( +  | ^ +  441 | expect.objectContaining({ id: 'deliver-evt-1' }), +  442 | expect.objectContaining({ address: contractConfig.address +}), +  443 | expect.any(String) + + at Object. (src/__tests__/notification-flow-e2e.test.ts:440:22) + + ● 3. Retry scenarios › prevents concurrent duplicate processing via distributed lock + + expect(jest.fn()).toHaveBeenCalledTimes(expected) + + Expected number of calls: 1 + Received number of calls: 0 + +   587 | await p1; +  588 | + > 589 | expect(sendMock).toHaveBeenCalledTimes(1); +  | ^ +  590 | const row = await db.get<{ +status: string }>( +  591 | 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] +  592 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:589:22) + + ● 3. Retry scenarios › retries a failed delivery on the next scheduler poll cycle + + expect(received).toBe(expected) // Object.is equality + + Expected: "COMPLETED" + Received: "PENDING" + +   620 | +  621 | const row = await repository.getById(id); + > 622 | expect(row!.status).toBe(NotificationSt +atus.COMPLETED); +  | ^ +  623 | expect(sendMock).toHaveBeenCalledTimes(2); +  624 | +  625 | await scheduler.stop(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:622:25) + + ● 4. Failure handling › marks a notification FAILED after exhausting all retries + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "DEAD_LETTERED" + +   664 | 'SELECT status, retry_count FROM scheduled_notifications WHERE id = +?', [id] +  665 | ); + > 666 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  667 | }); +  668 | +  669 | it('logs a permanent-failure error after max retries are exhausted', +async () => { + + at Object. (src/__tests__/notification-flow-e2e.test.ts:666:25) + + ● 4. Failure handling › logs a permanent-failure error after max retries are exhausted + + expect(jest.fn()).toHaveBeenCalledWith(...expected) + + Expected: "Notification permanently failed after max retries", ObjectContaining {"id": 2, "totalAttempts": 3} + Received: "Notification permanently failed after max retries", {"failureType": "unknown", "id": 2, "requestId": +"e2e-req-id", "totalAttempts": 4} + + Number of calls: 1 + +   677 | await retryScheduler.runOnce(); +  678 | + > 679 | expect(logger.error).toHaveBeenCalledWith( +  | ^ +  680 | 'Notification permanently failed after max retries', +  681 | expect.objectContaining({ id, totalAttempts: 3 }) +  682 | ); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:679:26) + + ● 4. Failure handling › handles a delivery that throws (network error) and schedules a retry + + expect(received).toBe(expected) // Object.is equality + + Expected: "PENDING" + Received: "PROCESSING" + +   713 | ); +  714 | // Still pending — will be retried later + > 715 | expect(row!.status).toBe(NotificationSt +atus.PENDING); +  | ^ +  716 | expect(row!.retry_count).toBe(2); +  717 | expect(row!.next_retry_at).not.toBeNull(); +  718 | }); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:715:25) + + ● 4. Failure handling › transitions scheduler-dispatched notification to FAILED when retries run out + + expect(received).toBe(expected) // Object.is equality + + Expected: "FAILED" + Received: "PROCESSING" + +   738 | +  739 | const row = await repository.getById(id); + > 740 | expect(row!.status).toBe(NotificationSt +atus.FAILED); +  | ^ +  741 | +  742 | await scheduler.stop(); +  743 | jest.useRealTimers(); + + at Object. (src/__tests__/notification-flow-e2e.test.ts:740:25) + + ● 4. Failure handling › recovers stale PROCESSING locks left by crashed schedulers + + SQLITE_ERROR: cannot commit - no transaction is active + +FAIL src/tests/events-server.test.ts (14.343 s) + ● Console + + console.log + 2026-10-16T12:52:05.866Z info: Handling GET /api/events +{"requestId":"4af6acaa","correlationId":"06652c5d-151d-40b7-9931-daf06f13625a","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.914Z info: GET /api/events complete +{"requestId":"4af6acaa","correlationId":"06652c5d-151d-40b7-9931-daf06f13625a","returned":0,"durationMs":56} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.942Z info: Handling GET /api/events +{"requestId":"8be9632d","correlationId":"my-trace-abc123","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.947Z info: GET /api/events complete +{"requestId":"8be9632d","correlationId":"my-trace-abc123","returned":0,"durationMs":6} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.955Z info: Handling GET /api/events +{"requestId":"23fee04e","correlationId":"15b916da-75dc-4312-8de7-e8e762f8db28","limit":20} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.966Z info: GET /api/events complete +{"requestId":"23fee04e","correlationId":"15b916da-75dc-4312-8de7-e8e762f8db28","returned":0,"durationMs":11} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.976Z warn: Unhandled request +{"requestId":"1ecd85ea","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:05.980Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":6,"requestId":"1ecd85ea"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:06.021Z warn: Unhandled request +{"requestId":"f17c60e0","correlationId":"7fefaee7-a664-4f15-87aa-8790bea6150f","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:06.026Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":5,"requestId":"f17c60e0"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:06.100Z warn: Unhandled request +{"requestId":"a5effff1","correlationId":"6a538acd-1c2f-4c7b-bace-8b555887f5ed","method":"GET","url":"/no-such-route"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + console.log + 2026-10-16T12:52:06.103Z info: Request completed +{"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":3,"requestId":"a5effff1"} + + at Console.log (node_modules/winston/lib/winston/transports/console.js:87:23) + + ● security headers › sets baseline headers on preflight responses without changing cache policy + + read ECONNRESET + + ● GET /api/events pagination › defaults to 20 items when limit is not provided + + TypeError: eventRegistry.addEvent is not a function + +   147 | const { eventRegistry } = +require('../store/event-registry'); +  148 | for (let i = 0; i < +150; i++) { + > 149 | eventRegistry.addEvent({ +  | ^ +  150 | eventId: `0000000000000000-${i}`, +  151 | contractAddress: +'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', +  152 | eventName: 'autoshare_created', + + at Object. (src/tests/events-server.test.ts:149:21) + + ● GET /api/events pagination › respects limit when provided within bounds + + TypeError: eventRegistry.addEvent is not a function + +   147 | const { eventRegistry } = +require('../store/event-registry'); +  148 | for (let i = 0; i < +150; i++) { + > 149 | eventRegistry.addEvent({ +  | ^ +  150 | eventId: `0000000000000000-${i}`, +  151 | contractAddress: +'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', +  152 | eventName: 'autoshare_created', + + at Object. (src/tests/events-server.test.ts:149:21) + + ● GET /api/events pagination › caps excessively large limit at 100 + + TypeError: eventRegistry.addEvent is not a function + +   147 | const { eventRegistry } = +require('../store/event-registry'); +  148 | for (let i = 0; i < +150; i++) { + > 149 | eventRegistry.addEvent({ +  | ^ +  150 | eventId: `0000000000000000-${i}`, +  151 | contractAddress: +'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', +  152 | eventName: 'autoshare_created', + + at Object. (src/tests/events-server.test.ts:149:21) + + ● GET /api/events pagination › handles negative limits by falling back to minimum (1) + + TypeError: eventRegistry.addEvent is not a function + +   147 | const { eventRegistry } = +require('../store/event-registry'); +  148 | for (let i = 0; i < +150; i++) { + > 149 | eventRegistry.addEvent({ +  | ^ +  150 | eventId: `0000000000000000-${i}`, +  151 | contractAddress: +'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', +  152 | eventName: 'autoshare_created', + + at Object. (src/tests/events-server.test.ts:149:21) + + ● GET /api/events pagination › handles invalid limit by falling back to default + + TypeError: eventRegistry.addEvent is not a function + +   147 | const { eventRegistry } = +require('../store/event-registry'); +  148 | for (let i = 0; i < +150; i++) { + > 149 | eventRegistry.addEvent({ +  | ^ +  150 | eventId: `0000000000000000-${i}`, +  151 | contractAddress: +'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', +  152 | eventName: 'autoshare_created', + + at Object. (src/tests/events-server.test.ts:149:21) + +A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due +to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure +that .unref() was called on them. +Test Suites: 3 failed, 3 total +Tests: 21 failed, 44 passed, 65 total +Snapshots: 0 total +Time: 52.98 s +Ran all test suites matching src/tests/events-server.test.ts|src/tests/notification-scheduler-refactored.test.ts|src/__ +tests__/notification-flow-e2e.test.ts. diff --git a/listener/_run4.txt b/listener/_run4.txt new file mode 100644 index 00000000..011aca61 --- /dev/null +++ b/listener/_run4.txt @@ -0,0 +1,123 @@ +2026-10-16T13:03:06.690Z info: Handling GET /api/events {"requestId":"ef77b472","correlationId":"0916edc1-b5a7-4fba-8f85-7114c61e8e87","limit":20} +2026-10-16T13:03:06.744Z info: GET /api/events complete {"requestId":"ef77b472","correlationId":"0916edc1-b5a7-4fba-8f85-7114c61e8e87","returned":0,"durationMs":65} +2026-10-16T13:03:06.767Z info: Handling GET /api/events {"requestId":"ad3a7637","correlationId":"my-trace-abc123","limit":20} +2026-10-16T13:03:06.768Z info: GET /api/events complete {"requestId":"ad3a7637","correlationId":"my-trace-abc123","returned":0,"durationMs":2} +2026-10-16T13:03:06.776Z info: Handling GET /api/events {"requestId":"d385bde8","correlationId":"56c7643d-94b9-473b-b72a-5328d521657d","limit":20} +2026-10-16T13:03:06.779Z info: GET /api/events complete {"requestId":"d385bde8","correlationId":"56c7643d-94b9-473b-b72a-5328d521657d","returned":0,"durationMs":3} +2026-10-16T13:03:06.786Z warn: Unhandled request {"requestId":"7596bcd1","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} +2026-10-16T13:03:06.788Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":2,"requestId":"7596bcd1"} +2026-10-16T13:03:06.821Z warn: Unhandled request {"requestId":"d8fda068","correlationId":"164fd73f-f204-44d4-a0d6-c23db7d565ad","method":"GET","url":"/no-such-route"} +2026-10-16T13:03:06.822Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":2,"requestId":"d8fda068"} +2026-10-16T13:03:06.999Z warn: Unhandled request {"requestId":"8632d83c","correlationId":"de9a8641-55fb-42bd-8159-651641f00402","method":"GET","url":"/no-such-route"} +2026-10-16T13:03:07.000Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":2,"requestId":"8632d83c"} +2026-10-16T13:03:07.036Z info: Handling GET /api/events {"requestId":"78223858","correlationId":"dc6021c7-c194-4a42-ad63-2e06a610a040","limit":50} +2026-10-16T13:03:07.037Z info: GET /api/events complete {"requestId":"78223858","correlationId":"dc6021c7-c194-4a42-ad63-2e06a610a040","returned":50,"durationMs":2} +2026-10-16T13:03:07.107Z info: Handling GET /api/events {"requestId":"4183ca02","correlationId":"35323213-73bb-4d93-aa36-d07f15038217","limit":100} +2026-10-16T13:03:07.111Z info: GET /api/events complete {"requestId":"4183ca02","correlationId":"35323213-73bb-4d93-aa36-d07f15038217","returned":100,"durationMs":5} +2026-10-16T13:03:07.117Z info: Handling GET /api/events {"requestId":"64ec5270","correlationId":"9be7f1aa-2ce4-489f-a82c-51f66f69c417","limit":1} +2026-10-16T13:03:07.118Z info: GET /api/events complete {"requestId":"64ec5270","correlationId":"9be7f1aa-2ce4-489f-a82c-51f66f69c417","returned":1,"durationMs":1} +2026-10-16T13:03:07.126Z info: Handling GET /api/events {"requestId":"a2d0c27b","correlationId":"0681b2dc-3923-4620-9260-4b35d339652c","limit":20} +2026-10-16T13:03:07.128Z info: GET /api/events complete {"requestId":"a2d0c27b","correlationId":"0681b2dc-3923-4620-9260-4b35d339652c","returned":20,"durationMs":1} +node : FAIL src/tests/events-server.test.ts (17.048 s) +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=1 --wor ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/tests/...t.ts (17.048 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + correlation ID propagation + √ generates a correlation ID when none is provided (240 ms) + √ echoes back the caller-supplied correlation ID (7 ms) + √ always includes x-request-id alongside correlation ID (10 ms) + √ correlation ID flows through on 404 responses (12 ms) + security headers + × sets baseline headers on preflight responses without changing cache policy (11 ms) + √ sets baseline headers on not-found responses (15 ms) + √ sets HSTS only when production is explicitly enabled (174 ms) + GET /api/events pagination + × defaults to 20 items when limit is not provided (14 ms) + × respects limit when provided within bounds (14 ms) + × caps excessively large limit at 100 (11 ms) + × handles negative limits by falling back to minimum (1) (6 ms) + × handles invalid limit by falling back to default (9 ms) + + ● security headers › sets baseline headers on preflight responses without changing cache policy + + read ECONNRESET + + ● GET /api/events pagination › defaults to 20 items when limit is not provided + + read ECONNRESET + + ● GET /api/events pagination › respects limit when provided within bounds + + TypeError: Cannot read properties of undefined (reading 'length') + +   175 | const { status, body } = await +makeRequest('/api/events?limit=50'); +  176 | expect(status).toBe(200); + > 177 | +expect(body.events.length).toBe(50); +  | ^ +  178 | }); +  179 | +  180 | test('caps excessively large limit at 100', async () +=> { + + at Object. (src/tests/events-server.test.ts:177:24) + + ● GET /api/events pagination › caps excessively large limit at 100 + + TypeError: Cannot read properties of undefined (reading 'length') + +   181 | const { status, body } = await +makeRequest('/api/events?limit=200'); +  182 | expect(status).toBe(200); + > 183 | +expect(body.events.length).toBe(100); +  | ^ +  184 | }); +  185 | +  186 | test('handles negative limits by falling back to minimum (1)', +async () => { + + at Object. (src/tests/events-server.test.ts:183:24) + + ● GET /api/events pagination › handles negative limits by falling back to minimum (1) + + TypeError: Cannot read properties of undefined (reading 'length') + +   187 | const { status, body } = await +makeRequest('/api/events?limit=-10'); +  188 | expect(status).toBe(200); + > 189 | +expect(body.events.length).toBe(1); +  | ^ +  190 | }); +  191 | +  192 | test('handles invalid limit by falling back to default', async +() => { + + at Object. (src/tests/events-server.test.ts:189:24) + + ● GET /api/events pagination › handles invalid limit by falling back to default + + TypeError: Cannot read properties of undefined (reading 'length') + +   193 | const { status, body } = await +makeRequest('/api/events?limit=invalid'); +  194 | expect(status).toBe(200); + > 195 | +expect(body.events.length).toBe(20); +  | ^ +  196 | }); +  197 | }); +  198 | + + at Object. (src/tests/events-server.test.ts:195:24) + +Test Suites: 1 failed, 1 total +Tests: 6 failed, 6 passed, 12 total +Snapshots: 0 total +Time: 19.739 s +Ran all test suites matching src/tests/events-server.test.ts. diff --git a/listener/_run5.txt b/listener/_run5.txt new file mode 100644 index 00000000..0ac31f3c --- /dev/null +++ b/listener/_run5.txt @@ -0,0 +1,133 @@ +2026-10-16T13:04:22.404Z info: Handling GET /api/events {"requestId":"035b50f4","correlationId":"71b855e8-90c7-4bac-a3bf-8a99011ff209","limit":20} +2026-10-16T13:04:22.436Z info: GET /api/events complete {"requestId":"035b50f4","correlationId":"71b855e8-90c7-4bac-a3bf-8a99011ff209","returned":0,"durationMs":37} +2026-10-16T13:04:22.459Z info: Handling GET /api/events {"requestId":"54552f5d","correlationId":"my-trace-abc123","limit":20} +2026-10-16T13:04:22.461Z info: GET /api/events complete {"requestId":"54552f5d","correlationId":"my-trace-abc123","returned":0,"durationMs":3} +2026-10-16T13:04:22.471Z info: Handling GET /api/events {"requestId":"af8df546","correlationId":"d5ad6291-39e9-4e79-92ba-df6433059b05","limit":20} +2026-10-16T13:04:22.473Z info: GET /api/events complete {"requestId":"af8df546","correlationId":"d5ad6291-39e9-4e79-92ba-df6433059b05","returned":0,"durationMs":1} +2026-10-16T13:04:22.484Z warn: Unhandled request {"requestId":"886893b3","correlationId":"trace-404-test","method":"GET","url":"/no-such-route"} +2026-10-16T13:04:22.486Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":3,"requestId":"886893b3"} +2026-10-16T13:04:22.513Z warn: Unhandled request {"requestId":"d891f625","correlationId":"cf829d79-28d4-4305-9a39-b4d69c026e35","method":"GET","url":"/no-such-route"} +2026-10-16T13:04:22.514Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":2,"requestId":"d891f625"} +2026-10-16T13:04:22.586Z warn: Unhandled request {"requestId":"ae2962df","correlationId":"84e66bba-c05d-45e5-b647-5a25c3871e95","method":"GET","url":"/no-such-route"} +2026-10-16T13:04:22.587Z info: Request completed {"method":"GET","url":"/no-such-route","statusCode":404,"durationMs":2,"requestId":"ae2962df"} +2026-10-16T13:04:22.603Z info: Handling GET /api/events {"requestId":"70109a26","correlationId":"d768b037-d668-4e4b-857f-7d34b8408e07","limit":20} +2026-10-16T13:04:22.605Z info: GET /api/events complete {"requestId":"70109a26","correlationId":"d768b037-d668-4e4b-857f-7d34b8408e07","returned":20,"durationMs":1} +2026-10-16T13:04:22.674Z info: Handling GET /api/events {"requestId":"38c0c97f","correlationId":"2136985e-29e8-466e-93a2-575936822344","limit":50} +2026-10-16T13:04:22.677Z info: GET /api/events complete {"requestId":"38c0c97f","correlationId":"2136985e-29e8-466e-93a2-575936822344","returned":50,"durationMs":2} +2026-10-16T13:04:22.687Z info: Handling GET /api/events {"requestId":"2515fb14","correlationId":"d68aacbc-ca65-4ec7-a21e-11f9d1268e17","limit":100} +2026-10-16T13:04:22.688Z info: GET /api/events complete {"requestId":"2515fb14","correlationId":"d68aacbc-ca65-4ec7-a21e-11f9d1268e17","returned":100,"durationMs":1} +2026-10-16T13:04:22.697Z info: Handling GET /api/events {"requestId":"b455295f","correlationId":"91d29512-effc-489a-b340-ab6d9b70322a","limit":1} +2026-10-16T13:04:22.698Z info: GET /api/events complete {"requestId":"b455295f","correlationId":"91d29512-effc-489a-b340-ab6d9b70322a","returned":1,"durationMs":1} +2026-10-16T13:04:22.705Z info: Handling GET /api/events {"requestId":"89c19a1d","correlationId":"a9622938-a7d8-4128-93fe-81a34b2bba42","limit":20} +2026-10-16T13:04:22.706Z info: GET /api/events complete {"requestId":"89c19a1d","correlationId":"a9622938-a7d8-4128-93fe-81a34b2bba42","returned":20,"durationMs":2} +node : FAIL src/tests/events-server.test.ts (8.83 s) +At line:15 char:3417 ++ ... pgrep\bin'; node ./node_modules/jest/bin/jest.js --maxWorkers=1 src/t ... ++ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (FAIL src/tests/...est.ts (8.83 s):String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + correlation ID propagation + √ generates a correlation ID when none is provided (93 ms) + √ echoes back the caller-supplied correlation ID (14 ms) + √ always includes x-request-id alongside correlation ID (11 ms) + √ correlation ID flows through on 404 responses (12 ms) + security headers + √ sets baseline headers on preflight responses without changing cache policy (8 ms) + √ sets baseline headers on not-found responses (11 ms) + √ sets HSTS only when production is explicitly enabled (71 ms) + GET /api/events pagination + × defaults to 20 items when limit is not provided (12 ms) + × respects limit when provided within bounds (13 ms) + × caps excessively large limit at 100 (10 ms) + × handles negative limits by falling back to minimum (1) (6 ms) + × handles invalid limit by falling back to default (7 ms) + + ● GET /api/events pagination › defaults to 20 items when limit is not provided + + TypeError: Cannot read properties of undefined (reading 'length') + +   168 | const { status, body } = await +makeRequest('/api/events'); +  169 | expect(status).toBe(200); + > 170 | +expect(body.events.length).toBe(20); +  | ^ +  171 | expect(body.count).toBeGreaterThanOrEqual(150); +  172 | }); +  173 | + + at Object. (src/tests/events-server.test.ts:170:24) + + ● GET /api/events pagination › respects limit when provided within bounds + + TypeError: Cannot read properties of undefined (reading 'length') + +   175 | const { status, body } = await +makeRequest('/api/events?limit=50'); +  176 | expect(status).toBe(200); + > 177 | +expect(body.events.length).toBe(50); +  | ^ +  178 | }); +  179 | +  180 | test('caps excessively large limit at 100', async () +=> { + + at Object. (src/tests/events-server.test.ts:177:24) + + ● GET /api/events pagination › caps excessively large limit at 100 + + TypeError: Cannot read properties of undefined (reading 'length') + +   181 | const { status, body } = await +makeRequest('/api/events?limit=200'); +  182 | expect(status).toBe(200); + > 183 | +expect(body.events.length).toBe(100); +  | ^ +  184 | }); +  185 | +  186 | test('handles negative limits by falling back to minimum (1)', +async () => { + + at Object. (src/tests/events-server.test.ts:183:24) + + ● GET /api/events pagination › handles negative limits by falling back to minimum (1) + + TypeError: Cannot read properties of undefined (reading 'length') + +   187 | const { status, body } = await +makeRequest('/api/events?limit=-10'); +  188 | expect(status).toBe(200); + > 189 | +expect(body.events.length).toBe(1); +  | ^ +  190 | }); +  191 | +  192 | test('handles invalid limit by falling back to default', async +() => { + + at Object. (src/tests/events-server.test.ts:189:24) + + ● GET /api/events pagination › handles invalid limit by falling back to default + + TypeError: Cannot read properties of undefined (reading 'length') + +   193 | const { status, body } = await +makeRequest('/api/events?limit=invalid'); +  194 | expect(status).toBe(200); + > 195 | +expect(body.events.length).toBe(20); +  | ^ +  196 | }); +  197 | }); +  198 | + + at Object. (src/tests/events-server.test.ts:195:24) + +Test Suites: 1 failed, 1 total +Tests: 5 failed, 7 passed, 12 total +Snapshots: 0 total +Time: 10.104 s, estimated 16 s +Ran all test suites matching src/tests/events-server.test.ts. diff --git a/listener/_typecheck-run.txt b/listener/_typecheck-run.txt new file mode 100644 index 00000000..cfbf99b4 --- /dev/null +++ b/listener/_typecheck-run.txt @@ -0,0 +1,4 @@ + +> listener@1.0.0 typecheck +> node ./node_modules/typescript/bin/tsc --noEmit + diff --git a/listener/package-lock.json b/listener/package-lock.json index 577b3c2a..4b540ab4 100644 --- a/listener/package-lock.json +++ b/listener/package-lock.json @@ -1,8447 +1,3 @@ -{ - "name": "listener", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "listener", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "cors": "^2.8.6", - "dotenv": "^17.4.2", - "express": "^5.2.1", - "node-cron": "^4.6.0", - "sqlite": "^5.1.1", - "sqlite3": "^6.0.1", - "winston": "^3.19.0" - }, - "devDependencies": { - "@swc/core": "^1.15.41", - "@swc/jest": "^0.2.39", - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "@types/jest": "^30.0.0", - "@types/node": "^26.1.1", - "@types/node-cache": "^4.1.3", - "@types/node-cron": "^3.0.11", - "@types/uuid": "^9.0.8", - "@types/winston": "^2.4.4", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", - "eslint": "^8.46.0", - "jest": "^30.4.2", - "prettier": "3.3.3", - "ts-jest": "^29.4.12", - "ts-node": "^10.9.2", - "typescript": "~5.7.2" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", - "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", - "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", - "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", - "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.9", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.1.tgz", - "integrity": "sha512-dTmUJzXSuayBK+hZydEaXd2mhx61qWQwkwaBBY6LyEOVx/L9aQU5ac8eFNEsd9nrD1+zb9zvDCphLSe8g1F4Qw==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.9.tgz", - "integrity": "sha512-R6siwR65Hm+3yfgP7o8DKhNvputQAwfoz9zTc3kyDudnomj2/BcLmD+uGQQPICjuFUp8ounPBU+jmKsocwVVAg==", - "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", - "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.15.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", - "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.5.2.tgz", - "integrity": "sha512-e8sQC4pCMHPBrPX7Hk8TJr+DM5dkUnImSTNag8SEWFAFAF6xIz4AJvCKapnrdHnZxep4bxw4PhG+ZbimYBMyKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.1", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.5.1", - "jest-util": "30.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.5.2.tgz", - "integrity": "sha512-/jqvFWoJF7LV1a6AUpYMbMm+2yIYsHJfR474H0SCzr9k8tnO/jl9wRg6zSG6lxgf6EozNlCG4amFzWsbZSvvZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.5.2", - "@jest/pattern": "30.5.0", - "@jest/reporters": "30.5.2", - "@jest/test-result": "30.5.2", - "@jest/transform": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.5.1", - "jest-config": "30.5.2", - "jest-haste-map": "30.5.1", - "jest-message-util": "30.5.1", - "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.1", - "jest-resolve-dependencies": "30.5.2", - "jest-runner": "30.5.2", - "jest-runtime": "30.5.2", - "jest-snapshot": "30.5.2", - "jest-util": "30.5.1", - "jest-validate": "30.5.1", - "jest-watcher": "30.5.2", - "pretty-format": "30.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/create-cache-key-function": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-30.5.1.tgz", - "integrity": "sha512-5qkif//qhlbt54255O/M2JtUZNlnefOzE12bi7FlApOIYO4pqrZcAwhNCfP4jOGFubez8uoLgY9czWFjYY57ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", - "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.5.2.tgz", - "integrity": "sha512-nwFPOUMbmsjNNRCpnJpB9HbSZu4C07wDDds++5j+j1MezzPLxMPDdbdUVjr2o4RULDOValnDd70taPFI1EtxAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "jest-mock": "30.5.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.5.2.tgz", - "integrity": "sha512-2E+1Pg6jJxbRjLNLpciC71iri/3qsKJL222/aXJEFwC0VdUSvfS+JZQQLEVjUYYc8t8pQu2MGE3cF2+bNTYRsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.5.2", - "jest-snapshot": "30.5.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz", - "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.5.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.5.2.tgz", - "integrity": "sha512-GKk0h0tKT5SpDPxDhPg3r9mm5s8/YCBzVNMOGdRiQXwUiMsjU10mVPlMaQstH6/cu50vOb8N9oQUMETHuwwxKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.1", - "@sinonjs/fake-timers": "^15.4.0", - "@types/node": "*", - "jest-message-util": "30.5.1", - "jest-mock": "30.5.2", - "jest-util": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz", - "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.5.2.tgz", - "integrity": "sha512-62zfS+dL+M5aTNHXCNLyqD+j4oqxTIzDYrZbpqMP8Yn+gvlAGyzC6BSPNb1ZmFOakV1RtFYX/O3FCQXSWmQdgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.5.2", - "@jest/expect": "30.5.2", - "@jest/types": "30.5.1", - "jest-mock": "30.5.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz", - "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.5.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/react-is-19": { - "name": "react-is", - "version": "19.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.1.tgz", - "integrity": "sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.5.2.tgz", - "integrity": "sha512-ev6E9iG73twQcfAfoTnviuYkr3yOrYbzAtMFCimv2fxHXPIp9PBt2u1wNMt0aYWgl8FPwIcS8oqRJg6alnht4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.5.2", - "@jest/test-result": "30.5.2", - "@jest/transform": "30.5.2", - "@jest/types": "30.5.1", - "@jridgewell/trace-mapping": "^0.3.31", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^13.0.6", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.5.1", - "jest-util": "30.5.1", - "jest-worker": "30.5.1", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", - "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.5.1.tgz", - "integrity": "sha512-V3wnxNtiVmw5PPVg433Cn3VdXnsOeu/ofLw3KC04Bn2y1wIlU5kozXQn48rhrgj/02LrCVtntTEF1yBha0XSUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.5.2.tgz", - "integrity": "sha512-c5CehvkbfHX6yyNg7bMn2ylqlp9AHXuZ/3hPH/Lh5bxD/vefd/lV9HBjMqXef9yQNqkdwyZvkLSVSwB38hy8Qw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "callsites": "^3.1.0", - "convert-source-map": "^2.0.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.5.2.tgz", - "integrity": "sha512-eO6YU5rsLfs60ne694e0IAmRgeBnoA8mu0nlxXDbl/1j5km0o2vO9/VbbUIKIH+WRKKEI+ELgwraRGn2Lhep8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.5.2", - "@jest/types": "30.5.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.5.2.tgz", - "integrity": "sha512-bmij8jZyYAC47ExT2GeP7PcrlwSNS+Bp3KeRuBH88gpcxqDv6fljN8PemS4J/lLZ79xb6mpaennQTTq0zUAVVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.5.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.5.2.tgz", - "integrity": "sha512-LBGwmaE886C41jDZ65eqR1YbXM7642MK3ZhTowZd5+xDlrDLuf4ePuDdDmVSvcr8NTVJ3MqxLkMbUCzuigUaoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.5.1", - "@jridgewell/trace-mapping": "^0.3.31", - "babel-plugin-istanbul": "^8.0.0", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.1", - "jest-regex-util": "30.5.0", - "jest-util": "30.5.1", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", - "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.5.0", - "@jest/schemas": "30.5.0", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", - "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", - "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.6.0", - "@parcel/watcher-darwin-arm64": "2.6.0", - "@parcel/watcher-darwin-x64": "2.6.0", - "@parcel/watcher-freebsd-x64": "2.6.0", - "@parcel/watcher-linux-arm-glibc": "2.6.0", - "@parcel/watcher-linux-arm-musl": "2.6.0", - "@parcel/watcher-linux-arm64-glibc": "2.6.0", - "@parcel/watcher-linux-arm64-musl": "2.6.0", - "@parcel/watcher-linux-x64-glibc": "2.6.0", - "@parcel/watcher-linux-x64-musl": "2.6.0", - "@parcel/watcher-win32-arm64": "2.6.0", - "@parcel/watcher-win32-x64": "2.6.0" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", - "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", - "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", - "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", - "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", - "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", - "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", - "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", - "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", - "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", - "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", - "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", - "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", - "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.34.52", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", - "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, - "node_modules/@swc/core": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.18.tgz", - "integrity": "sha512-0cYQLkpcwUngg55TDwpwrrVOU+B6zh1NFbp+oS7dVFhmYkzaARelwugo/bzPJh1FrcnppmF+KTqioCGwyqbcEw==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.28" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.16.18", - "@swc/core-darwin-x64": "1.16.18", - "@swc/core-linux-arm-gnueabihf": "1.16.18", - "@swc/core-linux-arm64-gnu": "1.16.18", - "@swc/core-linux-arm64-musl": "1.16.18", - "@swc/core-linux-ppc64-gnu": "1.16.18", - "@swc/core-linux-s390x-gnu": "1.16.18", - "@swc/core-linux-x64-gnu": "1.16.18", - "@swc/core-linux-x64-musl": "1.16.18", - "@swc/core-win32-arm64-msvc": "1.16.18", - "@swc/core-win32-ia32-msvc": "1.16.18", - "@swc/core-win32-x64-msvc": "1.16.18" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.18.tgz", - "integrity": "sha512-ozgiCHyFDCwfXzTnaLR+i8WxEyt06GTudn9rPh08vj+u3Z8vMvt5Pi8T+BikyNXm7y++h6UMdqsnJuWrV+tk2A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.18.tgz", - "integrity": "sha512-tuwxXi+sjxxTAi7GP3NeQXQxOf4SXSiFxKAMYLBdoy7JqGYby6uSDXojcFoLOhSp7oiNGstnaFXxfbP0Hpro9w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.18.tgz", - "integrity": "sha512-SMdhmY1P9Kveh0MRj6vqQF98bsjnDa0lBBvkd/lcpxmjj5QHJOQAU0N3g6h7GPWpBMs3h2g3lHu/QTDn+uMsXg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.18.tgz", - "integrity": "sha512-ep/mQyhblQYZ6tkfq0IJspcVSFl5jdywks3kejanrbB31uuujRD/QWOoGNp2L3lLRdAdQvgl/M3pA7g3hSuQug==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.18.tgz", - "integrity": "sha512-pkzGVxrAQu5+febW1a/Aiph3YJk0UiJKXoWbxnDVEIOZWkk5aETHTuyVsOQ3kOgn2dzNMvJVeqcoEQdH+ntqWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.18.tgz", - "integrity": "sha512-v9kiu4as1pJyQ8ESFTtMNydZ99m86QXLfAtm+XiKPKS93pXL80BCaXokAs9ebioGkIXgmwfbzUCdUC62H3JWkw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.18.tgz", - "integrity": "sha512-vU7TKRJ26i86YQr4QY/5bdcH72KzkBbSSUfPlErW6d6KOR47BcVGlYZFTcMMYlmd3CVSXFHW18tBSila66YfoA==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.18.tgz", - "integrity": "sha512-Ml6kbe0BoVJkSgmYPAEUudGcu84jaQeqDN8xeMumbwNpT0vlmm7241Ddp9XKCjQABvjomnXF0OiNOAfjBhNDNw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.18.tgz", - "integrity": "sha512-TGU24+QmmS8y/hg0BLKgOU3W5TCcQC9bgGl8AB0R61gTNQdWdcmCehRLAJwlZfxtAml66xMJ6PWrX8NKrABOsA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.18.tgz", - "integrity": "sha512-S53hm7Amc3YV8t2BUT6R7J+zSnwiGarG+GnxMca4W0uMkxBZ3Gl6jBwEgb6YqrYjf2DzOuRfauztqptCO76a/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.18.tgz", - "integrity": "sha512-+6/10+OJmU12zWkrPVG0QeQEr8VsWeF8gyXrOfHZMIfMOfz8/ULXVZPhV9GhGMKSWake1xEJj2IYCiFRS/xMpQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.16.18", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.18.tgz", - "integrity": "sha512-7LsVrIICAZWSajXT6DwO7PYSTnW9maamOCyQylaOh13Viptrgi/7hl9fP75SiHe+kscaJahGXcorgt4MOOh/1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/jest": { - "version": "0.2.39", - "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.39.tgz", - "integrity": "sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/create-cache-key-function": "^30.0.0", - "@swc/counter": "^0.1.3", - "jsonc-parser": "^3.2.0" - }, - "engines": { - "npm": ">= 7.0.0" - }, - "peerDependencies": { - "@swc/core": "*" - } - }, - "node_modules/@swc/types": { - "version": "0.1.28", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", - "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", - "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", - "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", - "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.6.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", - "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.9.0" - } - }, - "node_modules/@types/node-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/node-cache/-/node-cache-4.1.3.tgz", - "integrity": "sha512-3hsqnv3H1zkOhjygJaJUYmgz5+FcPO3vejBX7cE9/cnuINOJYrzkfOnUCvpwGe9kMZANIHJA7J5pOdeyv52OEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node-cron": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", - "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/winston": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.4.4.tgz", - "integrity": "sha512-BVGCztsypW8EYwJ+Hq+QNYiT/MUyCif0ouBH+flrY66O5W+KIXAMML6E/0fJpm7VjIzgangahl5S03bJJQGrZw==", - "deprecated": "This is a stub types definition. winston provides its own type definitions, so you do not need this installed.", - "dev": true, - "license": "MIT", - "dependencies": { - "winston": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", - "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", - "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/babel-jest": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.5.2.tgz", - "integrity": "sha512-ES8WeJ2fNWPEDunyAtUfJ12nHyaWUloCHdkfK2oW5uSoQSUmjNKdWiGaXJITJwvFEWoD/evHEg/QoCXAvLaQqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.5.2", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^8.0.0", - "babel-preset-jest": "30.5.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-8.0.0.tgz", - "integrity": "sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==", - "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^7.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.5.0.tgz", - "integrity": "sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.5.0.tgz", - "integrity": "sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.5.0", - "babel-preset-current-node-syntax": "^1.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1 || ^8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.29", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", - "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.29.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", - "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.11.26", - "caniuse-lite": "^1.0.30001813", - "electron-to-chromium": "^1.5.439", - "node-releases": "^2.0.57", - "update-browserslist-db": "^1.3.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.2.tgz", - "integrity": "sha512-V2o8YS2EhrKMO9AumnkK0ynynfFlsajpYupwq7S46H5mhcanwxDkNTw3K67wPIcKTH1ffkA4vhMOtAc7gRmPZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001816", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", - "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", - "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "license": "MIT", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", - "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.458", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.458.tgz", - "integrity": "sha512-/hIH2gRSmxrf4/X4Nw/t/Wc8PVS22l8/3HnWjWTWsTOM5dsnMUPZF/+u+yr3LqweNuTBJE1rq6qAQzKkZVwMfA==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/expect": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz", - "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.5.2", - "@jest/get-type": "30.5.0", - "jest-matcher-utils": "30.5.2", - "jest-message-util": "30.5.1", - "jest-mock": "30.5.2", - "jest-util": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/express": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", - "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.3.0", - "content-disposition": "^2.0.1", - "content-type": "^2.0.0", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.8", - "qs": "^6.16.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.1.0", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", - "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fb-watchman": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.4.tgz", - "integrity": "sha512-tGQtaoYisPDriJBN0mT5yZSyNETnJGA1gTYxaVRAE5KUO3LI9kPUc9WIu8JXcxaIci+faIKlzXqXyPgrmubtBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bser": "2.1.2" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/handlebars": { - "version": "4.7.10", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", - "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.8", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.5.2.tgz", - "integrity": "sha512-3gnpdBIza8ijCl3iMV9ChE3hSt1+46865qqod863gQtJr1DixOkAU5DoGbi3u+XlRcw++BhZQMu/y4xBdHBvSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.5.2", - "@jest/types": "30.5.1", - "import-local": "^3.2.0", - "jest-cli": "30.5.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.5.1.tgz", - "integrity": "sha512-0+bvMM/ENhDI29Z8q1r4HxiDIi4G5tnBmSw4esfPQoj9q8Nik7KIyuxHkTVnnniJQf05SxpGaKDQ+4h28ynGPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.5.1", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.5.2.tgz", - "integrity": "sha512-vffFgjs5JSJ9q4ds1vNW3klKYPJfYfIY61LT/zaZgvTeASJOWZUt8LBfHgIcV4rwxlPBLl/ePBGccHEclS4PlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.5.2", - "@jest/expect": "30.5.2", - "@jest/test-result": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.5.2", - "jest-matcher-utils": "30.5.2", - "jest-message-util": "30.5.1", - "jest-runtime": "30.5.2", - "jest-snapshot": "30.5.2", - "jest-util": "30.5.1", - "p-limit": "^3.1.0", - "pretty-format": "30.5.1", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-cli": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.5.2.tgz", - "integrity": "sha512-YYYxUUVjFgPvgEleKNKMaYGIFgR3l3832Cl0kRL/oCiDZ03v7wImUtquHl2OOBCCNgprjZgBN5bczS/JklPBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.5.2", - "@jest/test-result": "30.5.2", - "@jest/types": "30.5.1", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.5.2", - "jest-util": "30.5.1", - "jest-validate": "30.5.1", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.5.2.tgz", - "integrity": "sha512-U6221OZekabePvGcGPf4raIBKMvYQWFHvhobDJRojOYaFMj2HaGytAdknjDhYc6JTz2fdHW9+6k0wWUDaNeOFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.5.0", - "@jest/pattern": "30.5.0", - "@jest/test-sequencer": "30.5.2", - "@jest/types": "30.5.1", - "babel-jest": "30.5.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^13.0.6", - "graceful-fs": "^4.2.11", - "jest-circus": "30.5.2", - "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.2", - "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.1", - "jest-runner": "30.5.2", - "jest-util": "30.5.1", - "jest-validate": "30.5.1", - "parse-json": "^5.2.0", - "pretty-format": "30.5.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz", - "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.5.0", - "@jest/get-type": "30.5.0", - "chalk": "^4.1.2", - "pretty-format": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.5.0.tgz", - "integrity": "sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.5.2.tgz", - "integrity": "sha512-GRrW+7fmmgmkvyfEBqc1QFdWnF1Ex5yG6Y6AWL4TTxnkWlKIT98dsKh3P3UuWm6v4XsQwVE6DEzXIPMzWJ3ZbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.5.0", - "@jest/types": "30.5.1", - "chalk": "^4.1.2", - "jest-regex-util": "30.5.0", - "jest-util": "30.5.1", - "pretty-format": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.5.2.tgz", - "integrity": "sha512-Ciz4KgTGIbojxSKpuFImgWJj5EyqfE8xMq9NxWmOXnQywHHS03H9Mv+vaCB29J5pr3JBrY27WbRV18rBdjh8Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.5.2", - "@jest/fake-timers": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "jest-mock": "30.5.2", - "jest-util": "30.5.1", - "jest-validate": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.5.1.tgz", - "integrity": "sha512-VIFgt67jW480YDxKfEv9IYQKrFpYt7bOCMn3VsnjK7AK9qo7p6acpnA1DHwsVOULE3dTaYew/6JaTD/d0VDHoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.1", - "@parcel/watcher": "^2.6.0", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "fdir": "^6.5.0", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.5.0", - "jest-util": "30.5.1", - "jest-worker": "30.5.1", - "picomatch": "^4.0.3" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-leak-detector": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.5.1.tgz", - "integrity": "sha512-gt4GT2aWgEoCNTcBe4rqS74xTIUJxi+UD9SNSu9aOk5LmTEd6fS5KSTmAKAfitCCktQHNN+upUuL6EkBfFbMDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.5.0", - "pretty-format": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz", - "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.5.0", - "chalk": "^4.1.2", - "jest-diff": "30.5.2", - "pretty-format": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz", - "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.5.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.5.1", - "picomatch": "^4.0.3", - "pretty-format": "30.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-mock": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz", - "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "jest-util": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-regex-util": { - "version": "30.5.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", - "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.5.1.tgz", - "integrity": "sha512-wprhLejRtwN6h8ZgaqC0eYjGJ1uMdGPT/+b3eFncbG4NWuuP9QL+vWwRinu9waw7hkOLcpMJGVJAMgBwsPssMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.1", - "jest-util": "30.5.1", - "jest-validate": "30.5.1", - "slash": "^3.0.0", - "unrs-resolver": "^1.12.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.5.2.tgz", - "integrity": "sha512-GMI0DP5enX9Z2qW2zFy1bOkrCfWAOPRJQ7qHt0pdfrxBPsgWxglMPrRSK2TX/wRWYxcPRSLFg3Z88qPOzHx7FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "30.5.0", - "jest-snapshot": "30.5.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runner": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.5.2.tgz", - "integrity": "sha512-/B6px7hiiNhVSRwuv9AGn0nawYvwsHeQZ3ItTd5Gp2diFd0EOKaWVeWqcbw3L88vx1GfpCKjQkxWItZCTl11Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.5.2", - "@jest/environment": "30.5.2", - "@jest/source-map": "30.5.2", - "@jest/test-result": "30.5.2", - "@jest/transform": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.5.0", - "jest-environment-node": "30.5.2", - "jest-haste-map": "30.5.1", - "jest-leak-detector": "30.5.1", - "jest-message-util": "30.5.1", - "jest-resolve": "30.5.1", - "jest-runtime": "30.5.2", - "jest-util": "30.5.1", - "jest-watcher": "30.5.2", - "jest-worker": "30.5.1", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-runtime": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.5.2.tgz", - "integrity": "sha512-0paUAHBlheai10A3XSy9Xh+YBG0IROy+/qzcDPzQ/nbbXrznQzeWy5qniICWK6d3W3scaRQCVAbXdcK0ibBq4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.5.2", - "@jest/fake-timers": "30.5.2", - "@jest/globals": "30.5.2", - "@jest/source-map": "30.5.2", - "@jest/test-result": "30.5.2", - "@jest/transform": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.2.0", - "collect-v8-coverage": "^1.0.2", - "es-module-lexer": "^2.1.0", - "glob": "^13.0.6", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.5.1", - "jest-message-util": "30.5.1", - "jest-mock": "30.5.2", - "jest-regex-util": "30.5.0", - "jest-resolve": "30.5.1", - "jest-snapshot": "30.5.2", - "jest-util": "30.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.5.2.tgz", - "integrity": "sha512-jKIdes25AnAH73dOCi2qE6J6vHJ6L2vyBS11M89kT50DVJcLTf2AOjvcc+2vVaN8lUbY5v2R83aqXo3Ro9fqYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.5.2", - "@jest/get-type": "30.5.0", - "@jest/snapshot-utils": "30.5.1", - "@jest/transform": "30.5.2", - "@jest/types": "30.5.1", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.5.2", - "graceful-fs": "^4.2.11", - "jest-diff": "30.5.2", - "jest-matcher-utils": "30.5.2", - "jest-message-util": "30.5.1", - "jest-util": "30.5.1", - "pretty-format": "30.5.1", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-util": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", - "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.5.1", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.5.1.tgz", - "integrity": "sha512-i/buJ56wTpxihE93hQYNMfdOThS87+HGvLZzZJmU4xggPcdTYQq051iwALLCHp3q+SkCGH+EFejQZz5EbBSkkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.5.0", - "@jest/types": "30.5.1", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.5.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "30.5.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.5.2.tgz", - "integrity": "sha512-rqRgg4R11GA9m1UsEZwQeixHsgZReCn6TOHAHtIH33yXrIvoX0OGPA20Yzzjkz7X40fZvcRSxEpQppYPcWzmcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.5.2", - "@jest/types": "30.5.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.5.1", - "string-length": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.5.1.tgz", - "integrity": "sha512-Cbxh5v7AoLuFRmFJSM4/aHdQ68rjXvUWr716EE0Dh3I7T+T/3FgFKhOERGXHcU2Meftq9+9zxPM3TSNyI9D+HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.5.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/logform/node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", - "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", - "license": "MIT", - "dependencies": { - "content-type": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "3.99.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.99.0.tgz", - "integrity": "sha512-lLdwkeg2XUVPTHqWjPltTxH81zbQNbUWC+AEb9ni50KjxkUzAAx1HGtMT21sbntzfUGfIphhcklvs3imJeqDXw==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-cron": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.6.0.tgz", - "integrity": "sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==", - "license": "ISC", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", - "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.59", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", - "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", - "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-format": { - "version": "30.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", - "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/react-is-18": "npm:react-is@^18.3.1", - "@jest/react-is-19": "npm:react-is@^19.2.5", - "@jest/schemas": "30.5.0", - "ansi-styles": "^5.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", - "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/qs": { - "version": "6.16.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", - "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/sqlite": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", - "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==", - "license": "MIT" - }, - "node_modules/sqlite3": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz", - "integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^8.0.0", - "prebuild-install": "^7.1.3", - "tar": "^7.5.10" - }, - "engines": { - "node": ">=20.17.0" - }, - "optionalDependencies": { - "node-gyp": "12.x" - }, - "peerDependencies": { - "node-gyp": "12.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "8.9.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz", - "integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/synckit": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", - "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.3.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tar": { - "version": "7.5.22", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", - "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar-fs": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", - "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", - "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "license": "MIT", - "optional": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/ts-jest": { - "version": "29.4.15", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.15.tgz", - "integrity": "sha512-s2qmEUYFRqbjz1wv0EmccNPVOb45HOFpFuhzQJLKShYVisuTbCFPIIAIV2EuK5rIxb4BejLDbH7dnBZfYjybUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.10", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.8.5", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <9", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <7" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici": { - "version": "6.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", - "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unrs-resolver": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", - "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.4" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.12.2", - "@unrs/resolver-binding-android-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-x64": "1.12.2", - "@unrs/resolver-binding-freebsd-x64": "1.12.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", - "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-musl": "1.12.2", - "@unrs/resolver-binding-openharmony-arm64": "1.12.2", - "@unrs/resolver-binding-wasm32-wasi": "1.12.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", - "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/winston": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", - "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", diff --git a/listener/package.json b/listener/package.json index fd82918f..a64ad0e8 100644 --- a/listener/package.json +++ b/listener/package.json @@ -29,6 +29,7 @@ "license": "ISC", "description": "", "dependencies": { + "@stellar/stellar-sdk": "^15.1.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", diff --git a/listener/src/__tests__/load-test.workflow.test.ts b/listener/src/__tests__/load-test.workflow.test.ts index fdb2d58f..d6f4ea86 100644 --- a/listener/src/__tests__/load-test.workflow.test.ts +++ b/listener/src/__tests__/load-test.workflow.test.ts @@ -43,6 +43,7 @@ import { jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/__tests__/load.test.ts b/listener/src/__tests__/load.test.ts index fb1a9bd3..785e83ab 100644 --- a/listener/src/__tests__/load.test.ts +++ b/listener/src/__tests__/load.test.ts @@ -6,6 +6,7 @@ import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-b jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/__tests__/multi-channel-delivery.e2e.test.ts b/listener/src/__tests__/multi-channel-delivery.e2e.test.ts index 22900a86..121692cf 100644 --- a/listener/src/__tests__/multi-channel-delivery.e2e.test.ts +++ b/listener/src/__tests__/multi-channel-delivery.e2e.test.ts @@ -21,6 +21,7 @@ import { DiscordConfig, ContractConfig } from '../types'; jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/__tests__/notification-flow-e2e.test.ts b/listener/src/__tests__/notification-flow-e2e.test.ts index 117c9c42..a6c13685 100644 --- a/listener/src/__tests__/notification-flow-e2e.test.ts +++ b/listener/src/__tests__/notification-flow-e2e.test.ts @@ -44,10 +44,21 @@ const logger = jest.requireMock('../utils/logger').default; const TEST_DB_PATH = './data/test-notification-flow-e2e.db'; +/** Remove the shared test database file. On Windows the SQLite handle may not + * be released the instant `close()` resolves, so a failed unlink is tolerated: + * `initialize()` recreates the schema (IF NOT EXISTS) and `clearTables` resets data. */ +function removeDbFile(): void { + try { + if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + } catch { + /* transient lock — safe to ignore */ + } +} + async function setupDb(): Promise { const dbDir = path.dirname(TEST_DB_PATH); if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true }); - if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + removeDbFile(); const db = new Database(TEST_DB_PATH); await db.initialize(); return db; @@ -115,7 +126,7 @@ describe('1. Notification creation', () => { afterAll(async () => { await db.close(); - if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + removeDbFile(); }); beforeEach(async () => { @@ -167,21 +178,6 @@ describe('1. Notification creation', () => { }) ).rejects.toThrow('targetRecipient is required'); }); - afterEach(async () => { - await scheduler.stop(); - }); - - async function waitForSchedulerPolls(ms = 300): Promise { - await new Promise((resolve) => setTimeout(resolve, ms)); - } - - function futureExecuteAt(offsetMs = 10000): Date { - return new Date(Date.now() + offsetMs); - } - - describe('Complete notification lifecycle', () => { - it('should create, process, and deliver a notification', async () => { - const executeAt = futureExecuteAt(50); it('rejects a notification with an invalid payload', async () => { await expect( @@ -244,9 +240,6 @@ describe('1. Notification creation', () => { const stats = await repository.getStats(); expect(stats.pending).toBe(1); }); - await scheduler.start(); - await waitForSchedulerPolls(400); - it('rejects a duplicate idempotency key used with a different payload', async () => { const executeAt = new Date('2026-06-24T13:00:00.000Z'); const key = 'idem-create-2'; @@ -263,7 +256,7 @@ describe('1. Notification creation', () => { targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, executeAt }, undefined, key ) - ).rejects.toThrow('Idempotency key reused with different request body'); + ).rejects.toThrow('Idempotency key reused with a different request body'); }); it('cancels a scheduled notification', async () => { @@ -318,13 +311,11 @@ describe('2. Delivery workflows', () => { afterAll(async () => { await scheduler?.stop(); await db.close(); - if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + removeDbFile(); }); beforeEach(async () => { jest.clearAllMocks(); - jest.useFakeTimers(); - jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); await clearTables(db); sendMock = jest.fn().mockResolvedValue(true); @@ -334,39 +325,25 @@ describe('2. Delivery workflows', () => { afterEach(async () => { await scheduler.stop(); - jest.useRealTimers(); }); + // The scheduler loop is driven with a direct poll rather than fake timers: + // the native sqlite3 driver resolves queries on the libuv thread pool, which + // `jest.advanceTimersByTimeAsync()` cannot drain, so a faked clock would leave + // every query pending forever. + const poll = () => (scheduler as unknown as { processPendingNotifications(): Promise }) + .processPendingNotifications(); + it('processes a due notification and transitions it to COMPLETED', async () => { const id = await api.scheduleNotification({ payload: { message: 'deliver me' }, notificationType: NotificationType.DISCORD, targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, - executeAt: new Date('2026-06-24T12:00:02.000Z'), - it('should log execution attempts for audit trail', async () => { - const id = await api.scheduleNotification({ - payload: { message: 'Test notification' }, - notificationType: NotificationType.DISCORD, - targetRecipient: 'https://discord.com/webhook', - executeAt: futureExecuteAt(50), - }); - - await scheduler.start(); - await waitForSchedulerPolls(400); - - const logs = await db.all( - 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', - [id] - ); - - expect(logs).toHaveLength(1); - expect(logs[0].status).toBe('SUCCESS'); - expect(logs[0].execution_attempt).toBe(1); - expect(logs[0].scheduled_notification_id).toBe(id); + executeAt: new Date(Date.now() + 50), }); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); + await new Promise((resolve) => setTimeout(resolve, 80)); + await poll(); const row = await repository.getById(id); expect(row!.status).toBe(NotificationStatus.COMPLETED); @@ -378,16 +355,10 @@ describe('2. Delivery workflows', () => { payload: { message: 'not yet' }, notificationType: NotificationType.DISCORD, targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, - executeAt: new Date('2026-06-24T12:10:00.000Z'), // 10 min future + executeAt: new Date(Date.now() + 10 * 60_000), // 10 min future }); - describe('Idempotency handling', () => { - it('should return cached response for duplicate requests with same idempotency key', async () => { - const executeAt = futureExecuteAt(); - const payload = { message: 'Unique message' }; - const idempotencyKey = 'test-idempotency-key-1'; - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); + await poll(); const row = await repository.getById(id); expect(row!.status).toBe(NotificationStatus.PENDING); @@ -399,18 +370,14 @@ describe('2. Delivery workflows', () => { payload: { message: 'wait for it' }, notificationType: NotificationType.DISCORD, targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, - executeAt: new Date('2026-06-24T12:05:00.000Z'), + executeAt: new Date(Date.now() + 400), }); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(200); + await poll(); expect((await repository.getById(id))!.status).toBe(NotificationStatus.PENDING); - it('should reject duplicate requests with different payload', async () => { - const executeAt = futureExecuteAt(); - const idempotencyKey = 'test-idempotency-key-2'; - jest.setSystemTime(new Date('2026-06-24T12:05:01.000Z')); - await jest.advanceTimersByTimeAsync(400); + await new Promise((resolve) => setTimeout(resolve, 450)); + await poll(); expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); expect(sendMock).toHaveBeenCalledTimes(1); @@ -421,14 +388,11 @@ describe('2. Delivery workflows', () => { payload: { message: 'audit this' }, notificationType: NotificationType.DISCORD, targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, - executeAt: new Date('2026-06-24T12:00:02.000Z'), + executeAt: new Date(Date.now() + 50), }); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); - it('should clean up expired idempotency keys', async () => { - const executeAt = futureExecuteAt(); - const idempotencyKey = 'test-idempotency-key-3'; + await new Promise((resolve) => setTimeout(resolve, 80)); + await poll(); const logs = await db.all( 'SELECT * FROM notification_execution_log WHERE scheduled_notification_id = ?', @@ -440,20 +404,20 @@ describe('2. Delivery workflows', () => { }); it('processes a batch of multiple due notifications', async () => { - const executeAt = new Date('2026-06-24T12:00:02.000Z'); const ids = await Promise.all( Array.from({ length: 3 }, (_, i) => api.scheduleNotification({ payload: { message: `batch-${i}` }, notificationType: NotificationType.DISCORD, - targetRecipient: NotificationFixtureBuilder.constants.webhookUrl, - executeAt, + // Distinct recipients: batch validation rejects the same recipient twice. + targetRecipient: `${NotificationFixtureBuilder.constants.webhookUrl}/${i}`, + executeAt: new Date(Date.now() + 50), }) ) ); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); + await new Promise((resolve) => setTimeout(resolve, 80)); + await poll(); for (const id of ids) { expect((await repository.getById(id))!.status).toBe(NotificationStatus.COMPLETED); @@ -473,8 +437,7 @@ describe('2. Delivery workflows', () => { .build() ); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); + await poll(); expect(sendMock).toHaveBeenCalledWith( expect.objectContaining({ id: 'deliver-evt-1' }), @@ -496,22 +459,11 @@ describe('3. Retry scenarios', () => { beforeAll(async () => { db = await setupDb(); repository = new ScheduledNotificationRepository(db); - await db.run( - 'UPDATE idempotency_keys SET expires_at = ? WHERE idempotency_key = ?', - [new Date(Date.now() - 1000).toISOString(), idempotencyKey] - ); - - const cleanupCount = await idempotencyService.cleanupExpiredKeys(); - expect(cleanupCount).toBeGreaterThanOrEqual(1); - - stats = await idempotencyRepo.getStats(); - expect(stats.expired).toBeGreaterThanOrEqual(0); - }); }); afterAll(async () => { await db.close(); - if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + removeDbFile(); }); beforeEach(async () => { @@ -611,11 +563,6 @@ describe('3. Retry scenarios', () => { expect(sendMock).not.toHaveBeenCalled(); }); - describe('Integration tests', () => { - it('should handle high-volume notification creation with idempotency', async () => { - const executeAt = futureExecuteAt(); - const payload = { message: 'Batch test' }; - const idempotencyKey = 'batch-idempotency-1'; it('prevents concurrent duplicate processing via distributed lock', async () => { const id = await insertRetryable(repository, db, 1); @@ -637,6 +584,16 @@ describe('3. Retry scenarios', () => { const s2 = new RetryScheduler(repository, { ...RETRY_SCHEDULER_DEFAULTS, jitter: false }, discordService); const p1 = s1.runOnce(); + // Let s1 claim the row before s2 polls. `recoverStaleLocks()` opens a + // transaction, and two overlapping transactions on one SQLite connection + // would collide on BEGIN — so s2 must not start until s1 has claimed. + for (let attempt = 0; attempt < 200; attempt += 1) { + const claimed = await db.get<{ status: string }>( + 'SELECT status FROM scheduled_notifications WHERE id = ?', [id] + ); + if (claimed?.status === NotificationStatus.PROCESSING) break; + await new Promise((resolve) => setTimeout(resolve, 10)); + } await s2.runOnce(); // s2 should find nothing (s1 holds the lock) firstResolve(true); await p1; @@ -651,15 +608,20 @@ describe('3. Retry scenarios', () => { // -- NotificationScheduler retry via scheduler loop -- it('retries a failed delivery on the next scheduler poll cycle', async () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); - const sendMock = jest.fn() .mockResolvedValueOnce(false) // first attempt fails .mockResolvedValue(true); // second attempt succeeds const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; - const scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); + // retryDelayMs: 0 keeps the backoff window from pushing next_retry_at into + // the future, so the very next poll is allowed to pick the row back up. + const scheduler = new NotificationScheduler( + repository, + { ...SCHEDULER_CONFIG, retryDelayMs: 0 }, + discordService + ); + const poll = () => (scheduler as unknown as { processPendingNotifications(): Promise }) + .processPendingNotifications(); const id = await repository.create( NotificationFixtureBuilder.aScheduledNotificationInput() @@ -669,16 +631,12 @@ describe('3. Retry scenarios', () => { .build() ); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); // first poll — fails - await jest.advanceTimersByTimeAsync(250); // second poll — succeeds + await poll(); // first poll — fails + await poll(); // second poll — succeeds const row = await repository.getById(id); expect(row!.status).toBe(NotificationStatus.COMPLETED); expect(sendMock).toHaveBeenCalledTimes(2); - - await scheduler.stop(); - jest.useRealTimers(); }); }); @@ -697,7 +655,7 @@ describe('4. Failure handling', () => { afterAll(async () => { await db.close(); - if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + removeDbFile(); }); beforeEach(async () => { @@ -705,7 +663,7 @@ describe('4. Failure handling', () => { await clearTables(db); }); - it('marks a notification FAILED after exhausting all retries', async () => { + it('dead-letters a notification after exhausting all retries', async () => { const id = await insertRetryable(repository, db, 3, null, 3); // retryCount === maxRetries const retryScheduler = new RetryScheduler( repository, @@ -718,7 +676,7 @@ describe('4. Failure handling', () => { const row = await db.get<{ status: string; retry_count: number }>( 'SELECT status, retry_count FROM scheduled_notifications WHERE id = ?', [id] ); - expect(row!.status).toBe(NotificationStatus.FAILED); + expect(row!.status).toBe(NotificationStatus.DEAD_LETTERED); }); it('logs a permanent-failure error after max retries are exhausted', async () => { @@ -733,7 +691,7 @@ describe('4. Failure handling', () => { expect(logger.error).toHaveBeenCalledWith( 'Notification permanently failed after max retries', - expect.objectContaining({ id, totalAttempts: 3 }) + expect.objectContaining({ id, totalAttempts: 4 }) ); }); @@ -772,10 +730,7 @@ describe('4. Failure handling', () => { expect(row!.next_retry_at).not.toBeNull(); }); - it('transitions scheduler-dispatched notification to FAILED when retries run out', async () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date('2026-06-24T12:00:00.000Z')); - + it('dead-letters a scheduler-dispatched notification when retries run out', async () => { const sendMock = jest.fn().mockResolvedValue(false); const discordService = { sendEventNotification: sendMock } as unknown as DiscordNotificationService; const scheduler = new NotificationScheduler(repository, SCHEDULER_CONFIG, discordService); @@ -788,14 +743,11 @@ describe('4. Failure handling', () => { .build() ); - await scheduler.start(); - await jest.advanceTimersByTimeAsync(250); + await (scheduler as unknown as { processPendingNotifications(): Promise }) + .processPendingNotifications(); const row = await repository.getById(id); - expect(row!.status).toBe(NotificationStatus.FAILED); - - await scheduler.stop(); - jest.useRealTimers(); + expect(row!.status).toBe(NotificationStatus.DEAD_LETTERED); }); it('recovers stale PROCESSING locks left by crashed schedulers', async () => { @@ -880,7 +832,7 @@ describe('5. Backpressure and idempotency integration', () => { afterAll(async () => { await db.close(); - if (fs.existsSync(TEST_DB_PATH)) fs.unlinkSync(TEST_DB_PATH); + removeDbFile(); }); beforeEach(async () => { @@ -892,8 +844,6 @@ describe('5. Backpressure and idempotency integration', () => { }); afterEach(() => jest.useRealTimers()); - it('should maintain audit trail through complete lifecycle', async () => { - const executeAt = futureExecuteAt(50); it('activates backpressure when queue exceeds saturation threshold', () => { const isActive = backpressureController.checkAndApplyBackpressure(101); @@ -904,9 +854,6 @@ describe('5. Backpressure and idempotency integration', () => { expect(metrics.isActive).toBe(true); expect(metrics.targetThroughputPerSec).toBe(10); }); - await scheduler.start(); - await waitForSchedulerPolls(400); - it('calculates a positive processing delay under backpressure', () => { backpressureController.checkAndApplyBackpressure(101); const delay = backpressureController.calculateProcessingDelay(); diff --git a/listener/src/__tests__/query-performance.test.ts b/listener/src/__tests__/query-performance.test.ts deleted file mode 100644 index 6c19db01..00000000 --- a/listener/src/__tests__/query-performance.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, it, expect } from "vitest"; // if the repo uses jest, DELETE this line -import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { performance } from "node:perf_hooks"; - -// ADAPT 1: import the real registry/store and its types -// import { ... } from "../store/"; - -const DATASET_SIZE = Number(process.env.PERF_DATASET_SIZE ?? 10_000); -const WARMUP_RUNS = 5; -const MEASURED_RUNS = 30; -const ABS_FLOOR_MS = 2; // ignore sub-millisecond noise -const BASELINE_PATH = resolve(process.cwd(), "perf/query-baselines.json"); -const UPDATING = process.env.UPDATE_PERF_BASELINE === "1"; - -// ADAPT 2: fill the store with DATASET_SIZE realistic events -// (same shape the store really holds; vary contract ids and event types) -async function seed(): Promise { - for (let i = 0; i < DATASET_SIZE; i++) { - // e.g. registry.add({ id: `evt-${i}`, contractId: `C${i % 20}`, type: ..., ledger: i, ... }); - } -} - -// ADAPT 3: one entry per important query you found in Step 1 -const queries: { name: string; run: () => unknown | Promise }[] = [ - // { name: "events: latest page (limit 50)", run: () => registry.list({ limit: 50 }) }, - // { name: "events: filter by contract", run: () => registry.list({ contractId: "C3" }) }, - // { name: "events: lookup by id", run: () => registry.get(`evt-${DATASET_SIZE - 1}`) }, - // { name: "dedup: has-seen check", run: () => deduplicator.isDuplicate(`evt-${DATASET_SIZE - 1}`) }, -]; - -async function medianMs(fn: () => unknown | Promise): Promise { - for (let i = 0; i < WARMUP_RUNS; i++) await fn(); - const samples: number[] = []; - for (let i = 0; i < MEASURED_RUNS; i++) { - const start = performance.now(); - await fn(); - samples.push(performance.now() - start); - } - samples.sort((a, b) => a - b); - return samples[Math.floor(samples.length / 2)]; -} - -describe("query performance regression", () => { - it("keeps critical queries within baseline tolerance", async () => { - expect(queries.length).toBeGreaterThan(0); - await seed(); - - const measured: Record = {}; - for (const q of queries) measured[q.name] = await medianMs(q.run); - console.table( - Object.entries(measured).map(([name, ms]) => ({ name, medianMs: Number(ms.toFixed(3)) })), - ); - - if (UPDATING) { - mkdirSync(dirname(BASELINE_PATH), { recursive: true }); - const out = { - datasetSize: DATASET_SIZE, - tolerance: 3, - queries: Object.fromEntries( - Object.entries(measured).map(([k, v]) => [k, { medianMs: Number(v.toFixed(3)) }]), - ), - }; - writeFileSync(BASELINE_PATH, JSON.stringify(out, null, 2) + "\n"); - return; - } - - const baseline = JSON.parse(readFileSync(BASELINE_PATH, "utf8")); - for (const q of queries) { - const base = baseline.queries[q.name]; - expect(base, `no baseline for "${q.name}"; run npm run perf:baseline`).toBeDefined(); - const limit = Math.max(base.medianMs * baseline.tolerance, ABS_FLOOR_MS); - expect( - measured[q.name], - `"${q.name}" took ${measured[q.name].toFixed(3)}ms, limit ${limit.toFixed(3)}ms (baseline ${base.medianMs}ms x${baseline.tolerance})`, - ).toBeLessThanOrEqual(limit); - } - }, 60_000); -}); \ No newline at end of file diff --git a/listener/src/__tests__/stress.test.ts b/listener/src/__tests__/stress.test.ts index abf2c8db..b34adae8 100644 --- a/listener/src/__tests__/stress.test.ts +++ b/listener/src/__tests__/stress.test.ts @@ -23,6 +23,7 @@ import logger from '../utils/logger'; jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/api/__tests__/api-contract.test.ts b/listener/src/api/__tests__/api-contract.test.ts index 73d1bdb3..d5d34c47 100644 --- a/listener/src/api/__tests__/api-contract.test.ts +++ b/listener/src/api/__tests__/api-contract.test.ts @@ -49,7 +49,7 @@ jest.mock('../../store/event-registry', () => ({ // Mock logger jest.mock('../../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, sanitizeUrl: jest.fn((url: string) => url), })); diff --git a/listener/src/api/events-server.health.test.ts b/listener/src/api/events-server.health.test.ts index ec96b60d..97e1afe1 100644 --- a/listener/src/api/events-server.health.test.ts +++ b/listener/src/api/events-server.health.test.ts @@ -26,7 +26,7 @@ jest.mock('../store/event-registry', () => ({ jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); jest.mock('../database/database', () => ({ diff --git a/listener/src/api/events-server.history.test.ts b/listener/src/api/events-server.history.test.ts index 4d90e37a..aa5af602 100644 --- a/listener/src/api/events-server.history.test.ts +++ b/listener/src/api/events-server.history.test.ts @@ -46,7 +46,7 @@ jest.mock('../store/event-registry', () => ({ jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); const mockRegistry = eventRegistry as jest.Mocked; diff --git a/listener/src/api/events-server.test.ts b/listener/src/api/events-server.test.ts index 89a8915a..c916353c 100644 --- a/listener/src/api/events-server.test.ts +++ b/listener/src/api/events-server.test.ts @@ -45,7 +45,7 @@ jest.mock('../store/event-registry', () => ({ jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, sanitizeUrl: jest.fn((url: string) => url), })); diff --git a/listener/src/api/events-server.ts b/listener/src/api/events-server.ts index 98e0ef69..73b9d5e3 100644 --- a/listener/src/api/events-server.ts +++ b/listener/src/api/events-server.ts @@ -1,2018 +1,3 @@ -import http from 'http'; -import * as StellarSDK from '@stellar/stellar-sdk'; -export { getAppVersion } from '../utils/app-version'; -import { APP_VERSION } from '../utils/app-version'; -import { eventRegistry } from '../store/event-registry'; -import { preferenceStore } from '../store/preference-store'; -import { PreferencesUpdateInput } from '../types/preferences'; -import { NotificationAPI } from '../services/notification-api'; -import { NotificationType } from '../types/scheduled-notification'; -import logger from '../utils/logger'; -import { generateRequestId, resolveCorrelationId } from '../utils/request-id'; -import { TemplateService } from '../services/template-service'; -import { handleTemplateRoutes } from './template-routes'; -import { sendOk, sendErr, sendJson, ErrorCode } from '../utils/response'; -import { normalizePaginationParams } from '../utils/pagination'; -import { handleApiError, ApiError } from './error-handler'; -import { type RequestContext } from '../middleware/request-id'; -import { runWithRequestCorrelation } from '../middleware/correlation'; -import { sendOkWithETag } from '../middleware/etag'; -import { handleCredentialRoutes } from './credentials-api'; -import { authorizeRequest } from './api-key-auth'; -import { ApiCredentialService } from '../services/api-credential-service'; -import type { ApiScope } from '../types/api-credential'; -import { TemplateService } from '../services/template-service'; -import { handleTemplateRoutes } from './template-routes'; -import { sendOk, sendErr, sendJson, ErrorCode } from '../utils/response'; -import { validateContentType, getMimeType } from '../middleware/content-type'; -import { NotificationHistoryService } from '../services/notification-history'; -import { SearchSuggestionService } from '../services/search-suggestion'; -import { NotificationSearchService } from '../services/notification-search-service'; -import { collectRawBody, verifyWebhookRequest } from '../services/webhook-verifier'; -import { - IdempotencyKeyService, - IdempotencyKeyReuseError, -} from '../services/idempotency-key-service'; -import { WebhookReplayCache } from '../services/webhook-replay-cache'; -import { IdempotencyKeyService, IdempotencyKeyReuseError } from '../services/idempotency-key-service'; -import { WebhookSecret, RateLimitConfig, ContractConfig } from '../types'; -import { RateLimiter } from './rate-limiter'; -import { - normaliseApiPath, - applyVersionHeaders, - buildVersionCatalog, -} from './api-version-registry'; -import { getDatabase } from '../database/database'; -import { - getNotificationAnalyticsAggregator, - NotificationAnalyticsAggregator, -} from '../services/notification-analytics-aggregator'; -import { NotificationTemplateService } from '../services/notification-template-service'; -import { - TemplateNotFoundError, - TemplateValidationError, -} from '../services/notification-template-repository'; -import { TemplateRenderError } from '../services/notification-template-service'; -import { - parseTemplateUpdateBody, - resolveRequestActor, - serializeAuditRecord, - serializeTemplate, -} from './template-api'; -import { CreateNotificationTemplateInput } from '../types/notification-template'; -import { BatchValidationService } from '../services/batch-validation-service'; -import { handleArchiveRequest } from './archive-api'; -import { handleDeliveryMetricsRequest } from './delivery-metrics-api'; -import { ArchiveStore } from '../services/archive-store'; -import { ArchiveService } from '../services/archive-service'; -import { NotificationMetricsStore } from '../services/notification-metrics-store'; -import { NotificationHealthMonitor } from '../services/notification-health-monitor'; -import { getJobMonitor } from '../services/job-monitor'; -import { NotificationImportService } from '../services/notification-import-service'; -import { ResponseTimeMiddleware } from '../middleware/response-time'; -import { addSecurityHeaders } from '../middleware/security-headers'; -import { DEFAULT_MAX_BODY_BYTES, enforceBodyLimit } from '../middleware/body-limit'; -import { sanitizeUrl } from '../utils/logger'; -import { sanitizeCredentials } from '../utils/credential-sanitizer'; -import { DiscordNotificationProvider } from '../services/providers/discord-provider'; -import { WebhookNotificationProvider } from '../services/providers/webhook-provider'; -import { ProviderHealthResult } from '../types/provider-capabilities'; - -export interface EventsServerOptions { - port: number; - corsOrigin?: string; - isProduction?: boolean; - stellarRpcUrl: string; - stellarNetwork?: string; - stellarNetworkPassphrase?: string; - contractAddresses?: ContractConfig[]; - discordWebhookUrl?: string; - webhookSecrets?: WebhookSecret[]; - apiKeys?: Array<{ key: string; name?: string }>; - /** - * Issues and verifies scoped integration credentials (#988). Static - * `apiKeys` keep full access; credentials from this service are limited to - * their scopes. - */ - credentialService?: ApiCredentialService | null; - notificationAPI?: NotificationAPI | null; - /** Idempotency-Key replay protection for mutating endpoints (webhooks, schedule). */ - idempotencyService?: IdempotencyKeyService | null; - templateService?: NotificationTemplateService | null; - /** Scheduler-scoped template service, used to render templates for scheduled notifications. */ - schedulerTemplateService?: TemplateService | null; - rateLimit?: RateLimitConfig; - /** - * Optional override for the analytics aggregator. Tests use this to inject - * a controlled instance and reset state between cases. When omitted, the - * process-wide default aggregator is used. - */ - analyticsAggregator?: NotificationAnalyticsAggregator | null; - /** Archive store for retrieval endpoints (optional). */ - archiveStore?: ArchiveStore | null; - /** Archive service for the admin /run endpoint (optional). */ - archiveService?: ArchiveService | null; - /** Persisted metrics snapshots for historical analytics (optional). */ - metricsStore?: NotificationMetricsStore | null; - /** Maximum age of signed requests in seconds (default: 300 = 5 minutes). */ - signatureExpirationSeconds?: number; - /** - * Require `X-Webhook-Timestamp` on inbound webhooks (default: true). - * Without it a captured request is signed over the bare body and remains - * valid forever. Set to false only for legacy senders that cannot be changed. - */ - requireWebhookTimestamp?: boolean; - /** - * Maximum number of recently accepted webhook signatures retained by the - * replay cache (default: 10000). - */ - webhookReplayCacheMaxEntries?: number; - /** Optional health monitor — exposes its last report at GET /api/notifications/health. */ - healthMonitor?: NotificationHealthMonitor | null; - /** Receipt repository for the notification delivery status endpoint. */ - deliveryReceiptRepository?: DeliveryReceiptRepository | null; - /** - * Requests slower than this threshold (ms) are logged at WARN level (#491). - * Defaults to 1 000 ms. - */ - slowRequestThresholdMs?: number; - /** - * Override the ResponseTimeMiddleware instance — primarily for testing. - * When omitted a new instance is created using `slowRequestThresholdMs`. - */ - responseTimeMiddleware?: ResponseTimeMiddleware | null; - /** - * Largest request body accepted, in bytes. Oversized requests get a 413 and - * are never parsed. Defaults to {@link DEFAULT_MAX_BODY_BYTES}. - */ - maxBodyBytes?: number; - /** Optional DataExportService override for administrative exports (#850). */ - dataExportService?: DataExportService | null; -} - -type ServiceStatus = 'ok' | 'error' | 'not_configured'; - -interface ServiceHealth { - status: ServiceStatus; - latencyMs?: number; - detail?: string; -} - -interface HealthResponse { - status: 'ok' | 'degraded' | 'error'; - /** Semver string sourced from listener/package.json, e.g. "1.0.0". */ - version: string; - timestamp: string; - uptimeSeconds: number; - network: string; - services: { - stellarRpc: ServiceHealth; - discord: ServiceHealth; - database: ServiceHealth; - eventRegistry: { status: ServiceStatus; eventCount: number }; - }; -} - -const HEALTH_TIMEOUT_MS = 5000; -const NETWORK_TIP_CACHE_TTL_MS = 2000; - -type IndexingStatus = 'synced' | 'syncing' | 'degraded'; - -interface IndexingHealthResponse { - status: IndexingStatus; - timestamp: string; - indexedLedger: number | null; - networkTipLedger: number | null; - ledgerLag: number | null; - /** - * Time since the last event was ingested into the in-memory registry. - * This serves as a lightweight proxy for ingestion latency / pipeline stalls. - */ - processingDelayMs: number | null; - lastIngestedAt: string | null; - detail?: string; -} - -let cachedNetworkTip: { fetchedAt: number; ledger: number | null; errorDetail?: string } | null = - null; - -function withTimeout(promise: Promise, ms: number): Promise { - return Promise.race([ - promise, - new Promise((_, reject) => { - const t = setTimeout(() => reject(new Error('Health check timed out')), ms); - if (t && typeof (t as any).unref === 'function') (t as any).unref(); - }), - ]); -} - -export async function checkStellarRpc(rpcUrl: string): Promise { - const start = Date.now(); - try { - const server = new StellarSDK.rpc.Server(rpcUrl); - await withTimeout(server.getHealth(), HEALTH_TIMEOUT_MS); - return { status: 'ok', latencyMs: Date.now() - start }; - } catch (err) { - return { - status: 'error', - latencyMs: Date.now() - start, - detail: err instanceof Error ? err.message : String(err), - }; - } -} - -export async function checkDiscord(webhookUrl: string): Promise { - const start = Date.now(); - try { - const response = await withTimeout(fetch(webhookUrl, { method: 'GET' }), HEALTH_TIMEOUT_MS); - if (response.ok) { - return { status: 'ok', latencyMs: Date.now() - start }; - } - return { - status: 'error', - latencyMs: Date.now() - start, - detail: `HTTP ${response.status}`, - }; - } catch (err) { - return { - status: 'error', - latencyMs: Date.now() - start, - detail: err instanceof Error ? err.message : String(err), - }; - } -} - -export async function checkDatabase(): Promise { - const start = Date.now(); - try { - const db = getDatabase(); - if (!db.isConnected()) { - return { - status: 'error', - latencyMs: Date.now() - start, - detail: 'Database not initialized', - }; - } - await withTimeout(db.get('SELECT 1 AS ok'), HEALTH_TIMEOUT_MS); - return { status: 'ok', latencyMs: Date.now() - start }; - } catch (err) { - return { - status: 'error', - latencyMs: Date.now() - start, - detail: err instanceof Error ? err.message : String(err), - }; - } -} - -async function getContractPauseStatus( - contractAddress: string, - stellarRpcUrl: string, -): Promise<{ paused: boolean; error?: string }> { - try { - const server = new StellarSDK.rpc.Server(stellarRpcUrl); - const contract = new StellarSDK.Contract(contractAddress); - - // Create a dummy account for simulation (we don't need to actually sign anything) - const dummyKeypair = StellarSDK.Keypair.random(); - const sourceAccount = await server.getAccount(dummyKeypair.publicKey()).catch(() => { - // If the dummy account doesn't exist, we can still simulate - return new StellarSDK.Account(dummyKeypair.publicKey(), '0'); - }); - - const tx = new StellarSDK.TransactionBuilder(sourceAccount, { - fee: StellarSDK.BASE_FEE, - networkPassphrase: 'Test SDF Network ; September 2015', // We just need this for simulation - }) - .addOperation(contract.call('get_paused_status')) - .setTimeout(30) - .build(); - - const simulation = await server.simulateTransaction(tx); - - // Check if simulation was successful by looking for error property - if ('error' in simulation && simulation.error) { - const errorMsg = - typeof simulation.error === 'object' && 'message' in simulation.error - ? (simulation.error as any).message - : 'Failed to simulate contract call'; - return { - paused: false, - error: errorMsg, - }; - } - - // At this point, simulation is successful and has a result property - const simResult = (simulation as any).result; - const value = StellarSDK.scValToNative(simResult.retval); - return { paused: !!value }; - } catch (err) { - return { - paused: false, - error: err instanceof Error ? err.message : String(err), - }; - } -} - -async function buildStatusResponse(options: EventsServerOptions): Promise<{ - contracts: Array<{ - address: string; - paused: boolean; - error?: string; - }>; - timestamp: string; -}> { - const contractStatuses = await Promise.all( - (options.contractAddresses ?? []).map(async (contractConfig) => { - const status = await getContractPauseStatus(contractConfig.address, options.stellarRpcUrl); - return { - address: contractConfig.address, - ...status, - }; - }), - ); - - return { - timestamp: new Date().toISOString(), - contracts: contractStatuses, - }; -} - -async function fetchNetworkTipLedger(rpcUrl: string): Promise<{ - ledger: number | null; - errorDetail?: string; -}> { - if (cachedNetworkTip && Date.now() - cachedNetworkTip.fetchedAt < NETWORK_TIP_CACHE_TTL_MS) { - return { ledger: cachedNetworkTip.ledger, errorDetail: cachedNetworkTip.errorDetail }; - } - - const start = Date.now(); - try { - const server = new StellarSDK.rpc.Server(rpcUrl); - - // `getLatestLedger` is the most direct source of the current ledger/tip for Soroban RPC. - // We keep extraction defensive to avoid hard-coupling to the SDK response shape. - const latest: any = await withTimeout( - (server as any).getLatestLedger(), - HEALTH_TIMEOUT_MS, - ); - const ledger = - typeof latest?.sequence === 'number' - ? latest.sequence - : typeof latest?.ledger === 'number' - ? latest.ledger - : typeof latest?.latestLedger === 'number' - ? latest.latestLedger - : null; - - cachedNetworkTip = { fetchedAt: Date.now(), ledger }; - return { ledger }; - } catch (err) { - const errorDetail = err instanceof Error ? err.message : String(err); - cachedNetworkTip = { fetchedAt: Date.now(), ledger: null, errorDetail }; - logger.warn('Failed to fetch network tip ledger', { - rpcUrl, - durationMs: Date.now() - start, - errorDetail, - }); - return { ledger: null, errorDetail }; - } -} - -function deriveIndexingStatus(args: { - indexedLedger: number | null; - networkTipLedger: number | null; - processingDelayMs: number | null; -}): { status: IndexingStatus; detail?: string } { - const { indexedLedger, networkTipLedger, processingDelayMs } = args; - - if (networkTipLedger === null) { - return { status: 'degraded', detail: 'Unable to resolve network tip ledger.' }; - } - - if (indexedLedger === null) { - return { status: 'syncing', detail: 'No events ingested yet.' }; - } - - const ledgerLag = Math.max(0, networkTipLedger - indexedLedger); - const delay = processingDelayMs ?? Number.POSITIVE_INFINITY; - - if (ledgerLag === 0 && delay <= 60_000) { - return { status: 'synced' }; - } - - if (ledgerLag <= 5 && delay <= 5 * 60_000) { - return { status: 'syncing', detail: `Behind by ${ledgerLag} ledger(s).` }; - } - - return { - status: 'degraded', - detail: `Behind by ${ledgerLag} ledger(s) and last ingestion was ${Math.round( - delay / 1000, - )}s ago.`, - }; -} - -async function buildHealthResponse(options: EventsServerOptions): Promise { - const [stellarRpc, discord, database] = await Promise.all([ - checkStellarRpc(options.stellarRpcUrl), - options.discordWebhookUrl - ? checkDiscord(options.discordWebhookUrl) - : Promise.resolve({ status: 'not_configured' }), - checkDatabase(), - ]); - - const eventRegistryHealth = { - status: 'ok' as ServiceStatus, - eventCount: eventRegistry.count(), - }; - - let overallStatus: HealthResponse['status']; - if (stellarRpc.status === 'error' || database.status === 'error') { - overallStatus = 'error'; - } else if (discord.status === 'error') { - overallStatus = 'degraded'; - } else { - overallStatus = 'ok'; - } - - return { - status: overallStatus, - version: APP_VERSION, - timestamp: new Date().toISOString(), - uptimeSeconds: process.uptime(), - network: options.stellarNetwork ?? 'unknown', - services: { - stellarRpc, - discord, - database, - eventRegistry: eventRegistryHealth, - }, - }; -} - -function isRateLimitExempt(pathname: string): boolean { - return pathname === '/health' || pathname === '/api/rate-limit/metrics'; -} - -export function createEventsServer(options: EventsServerOptions): http.Server { - const corsOrigin = options.corsOrigin ?? 'http://localhost:5173'; - const historyService = new NotificationHistoryService(); - const suggestionService = new SearchSuggestionService(); - const notificationSearchService = new NotificationSearchService(); - const rateLimiter = options.rateLimit ? new RateLimiter(options.rateLimit) : undefined; - // Replay protection (#853): remembers every signature we accepted inside the - // freshness window so a captured request cannot be resubmitted. Kept - // independent of the optional client-supplied `Idempotency-Key` header, - // which an attacker replaying a request would simply omit. - const webhookReplayCache = new WebhookReplayCache({ - maxEntries: options.webhookReplayCacheMaxEntries ?? 10_000, - }); - // Response-time tracking (#491) - const responseTime = - options.responseTimeMiddleware !== undefined && options.responseTimeMiddleware !== null - ? options.responseTimeMiddleware - : new ResponseTimeMiddleware({ slowRequestThresholdMs: options.slowRequestThresholdMs }); - - const handleRequest = async ( - req: http.IncomingMessage, - res: http.ServerResponse, - requestContext: RequestContext, - ): Promise => { - addSecurityHeaders(res, { - isProduction: options.isProduction ?? process.env.NODE_ENV === 'production', - }); - - const { requestId, correlationId } = requestContext; - const startTime = Date.now(); - - // Start response-time tracking for this request (#491) - responseTime.start(res); - - // Request body size limit. - // - // Screened here, before routing, so an oversized payload is answered and - // the socket destroyed before any route handler attaches its own `data` - // listener and starts accumulating the body in memory. - const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES; - const bodyLimit = enforceBodyLimit(req, res, { - maxBytes: maxBodyBytes, - onRejected: (reason, observedBytes) => { - logger.warn('Request body exceeded size limit', { - requestId, - correlationId, - method: req.method, - url: sanitizeUrl(req.url ?? '/'), - reason, - observedBytes, - maxBytes: maxBodyBytes, - }); - }, - }); - if (!bodyLimit.allowed) { - return; - } - - res.setHeader('Access-Control-Allow-Origin', corsOrigin); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.setHeader( - 'Access-Control-Allow-Headers', - 'Content-Type, X-API-Key, Authorization, X-Correlation-Id, X-Request-Id, If-None-Match', - ); - res.setHeader( - 'Access-Control-Expose-Headers', - 'X-Request-Id, X-Correlation-Id, X-Response-Time, ETag', - ); - - const url = new URL(req.url ?? '/', 'http://localhost'); - const pathname = url.pathname; - - // ── API Route Versioning (#386, #984) ────────────────────────────────── - // Normalise /api/vN/* → /api/* using the central version registry so that - // every route handler operates on the canonical path form. The registry - // also applies X-API-Version, Deprecation, Sunset, and Link headers - // automatically based on the ROUTE_CATALOG lifecycle status. - { - const { normalisedPath } = normaliseApiPath(url.pathname); - url.pathname = normalisedPath; - } - applyVersionHeaders(req.method ?? 'GET', url.pathname, res); - - // Standardize response Content-Type for all API responses (#647) - if (req.method !== 'OPTIONS') { - res.setHeader('Content-Type', 'application/json'); - } - /** - * Enforces X-API-Key auth and scope checks (#988) for protected endpoints. - * Sends 401 UNAUTHORIZED or 403 INVALID_PERMISSIONS and resolves false - * when the request may not proceed. - */ - const requireApiKey = async (requiredScopes: readonly ApiScope[] = []): Promise => { - const auth = await authorizeRequest(req, { - apiKeys: options.apiKeys, - credentialService: options.credentialService, - requiredScopes, - }); - if (auth.allowed) return true; - logger.warn('API key authorization failed', { - requestId, - correlationId, - method: req.method, - path: url.pathname, - status: auth.status, - code: auth.code, - }); - if (auth.status === 401) res.setHeader('WWW-Authenticate', 'ApiKey header="X-API-Key"'); - sendErr(res, auth.status, auth.message, auth.code, auth.details); - return false; - }; - - // The rate-limit metrics endpoint is an observability route and must stay - // reachable even after a client exhausts its quota — otherwise callers - // can't read the very metrics that explain why they are being throttled. - const isRateLimitExempt = - req.method === 'GET' && (pathname === '/api/rate-limit/metrics' || pathname === '/health'); - - if (rateLimiter && !isRateLimitExempt) { - const allowed = await rateLimiter.handle(req, res as any); - if (!allowed) return; - } - - if (req.method === 'OPTIONS') { - res.removeHeader('Content-Type'); - res.writeHead(204); - res.end(); - return; - } - - // Scoped API credential management (#988) - if (url.pathname === '/api/credentials' || url.pathname.startsWith('/api/credentials/')) { - await handleCredentialRoutes(req, res, url, { - credentialService: options.credentialService, - apiKeys: options.apiKeys, - }); - return; - } - - // Template API routes (handled first for priority) - // Note: route matching is handled inline below for NotificationTemplateService compatibility. - // url.pathname is already rewritten from /api/v1/* → /api/* above - if (options.schedulerTemplateService && url.pathname.startsWith('/api/templates')) { - handleTemplateRoutes(req, res, requestId, options.schedulerTemplateService) - .then((handled) => { - if (!handled) { - sendErr(res, 404, 'Not found', ErrorCode.NOT_FOUND); - } - }) - .catch((error) => { - logger.error('Template route handler error', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } - - // GET /health - if (req.method === 'GET' && url.pathname === '/health') { - buildHealthResponse(options) - .then((health) => { - const httpStatus = health.status === 'error' ? 503 : 200; - sendJson(res, httpStatus, health); - }) - .catch((err) => { - logger.error('Health check failed unexpectedly', { - error: err, - requestId, - correlationId, - }); - sendErr(res, 500, 'Internal health check failure', ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/status - if (req.method === 'GET' && url.pathname === '/api/status') { - buildStatusResponse(options) - .then((status) => { - sendOk(res, 200, status); - }) - .catch((err) => { - logger.error('Status check failed unexpectedly', { - error: err, - requestId, - correlationId, - }); - sendErr(res, 500, 'Internal status check failure', ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/events/history?fromLedger=&toLedger= - // - // Returns events from the in-process registry whose ledger number falls - // within the requested inclusive range. Both query parameters are - // required; the range must be well-formed and must not exceed - // MAX_LEDGER_RANGE (10 000) ledgers. - if (req.method === 'GET' && url.pathname === '/api/events/history') { - const fromParam = url.searchParams.get('fromLedger'); - const toParam = url.searchParams.get('toLedger'); - - let range: { startLedger: number; endLedger: number }; - try { - range = parseLedgerRangeParams(fromParam, toParam); - } catch (err) { - if (err instanceof ValidationError) { - sendErr(res, 400, 'Invalid ledger range', ErrorCode.BAD_REQUEST, validationErrorBody(err)); - } else { - sendErr(res, 400, 'Invalid ledger range', ErrorCode.BAD_REQUEST); - } - return; - } - - const events = eventRegistry.getEventsByLedgerRange(range.startLedger, range.endLedger); - - logger.info('Handling GET /api/events/history', { - requestId, - correlationId, - startLedger: range.startLedger, - endLedger: range.endLedger, - returned: events.length, - }); - - sendOkWithETag(req, res, { - count: events.length, - startLedger: range.startLedger, - endLedger: range.endLedger, - events, - }); - - logger.info('GET /api/events/history complete', { - requestId, - correlationId, - startLedger: range.startLedger, - endLedger: range.endLedger, - returned: events.length, - durationMs: Date.now() - startTime, - }); - return; - } - - // GET /api/version — version catalog and deprecation registry (#984) - if (req.method === 'GET' && url.pathname === '/api/version') { - sendOk(res, 200, buildVersionCatalog()); - return; - } - - // GET /api/events - if (req.method === 'GET' && url.pathname.startsWith('/api/events')) { - const limitParam = url.searchParams.get('limit'); - const parsedLimit = limitParam ? parseInt(limitParam, 10) : undefined; - const paginationParams = normalizePaginationParams( - parsedLimit !== undefined && !Number.isNaN(parsedLimit) ? parsedLimit : undefined - ); - const events = eventRegistry.getEvents(paginationParams.limit); - - logger.info('Handling GET /api/events', { requestId, correlationId, limit: paginationParams.limit }); - - // ETag (#985): pollers that already hold the current feed get a 304. - sendOkWithETag(req, res, { count: eventRegistry.count(), events }); - - logger.info('GET /api/events complete', { - requestId, - correlationId, - returned: events.length, - durationMs: Date.now() - startTime, - }); - return; - } - - // GET /api/indexing/health - if (req.method === 'GET' && url.pathname === '/api/indexing/health') { - const networkTip = await fetchNetworkTipLedger(options.stellarRpcUrl); - const ingestion = eventRegistry.getIngestionSnapshot(); - - const now = Date.now(); - const processingDelayMs = - ingestion.lastIngestedAt === null ? null : Math.max(0, now - ingestion.lastIngestedAt); - - const indexedLedger = ingestion.lastIngestedLedger; - const networkTipLedger = networkTip.ledger; - const ledgerLag = - indexedLedger === null || networkTipLedger === null - ? null - : Math.max(0, networkTipLedger - indexedLedger); - - const derived = deriveIndexingStatus({ indexedLedger, networkTipLedger, processingDelayMs }); - - const response: IndexingHealthResponse = { - status: derived.status, - timestamp: new Date().toISOString(), - indexedLedger, - networkTipLedger, - ledgerLag, - processingDelayMs, - lastIngestedAt: ingestion.lastIngestedAt - ? new Date(ingestion.lastIngestedAt).toISOString() - : null, - detail: derived.detail ?? networkTip.errorDetail, - }; - - sendOk(res, 200, response); - return; - } - - // GET /api/analytics - if (req.method === 'GET' && url.pathname === '/api/analytics') { - const aggregator = - options.analyticsAggregator !== undefined - ? options.analyticsAggregator - : getNotificationAnalyticsAggregator(); - - if (!aggregator) { - sendErr(res, 503, 'Analytics aggregator unavailable', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const snapshot = aggregator.snapshot(); - const reset = url.searchParams.get('reset') === 'true'; - - logger.info('Handling GET /api/analytics', { - requestId, - correlationId, - totalRecorded: snapshot.totalRecorded, - reset, - durationMs: Date.now() - startTime, - }); - - // A resetting read has a side effect, so it always returns the full body. - if (!reset) { - sendOkWithETag(req, res, snapshot); - return; - } - - sendOk(res, 200, snapshot); - aggregator.reset(); - logger.info('Analytics snapshot reset after read', { requestId, correlationId }); - return; - } - - // GET /api/analytics/history - if (req.method === 'GET' && url.pathname === '/api/analytics/history') { - if (!options.metricsStore) { - sendErr(res, 503, 'Metrics store unavailable', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - const limitParam = url.searchParams.get('limit'); - const sinceParam = url.searchParams.get('since'); - const limit = limitParam ? parseInt(limitParam, 10) : undefined; - const since = sinceParam ? new Date(sinceParam) : undefined; - options.metricsStore - .getHistory(limit, since) - .then((snapshots) => { - sendOkWithETag(req, res, { snapshots }); - }) - .catch((error) => { - logger.error('Failed to fetch metrics history', { error, requestId, correlationId }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/notifications/health - if (req.method === 'GET' && url.pathname === '/api/notifications/health') { - const report = options.healthMonitor?.getLastReport() ?? null; - if (!report) { - sendJson(res, 503, { error: 'Health monitor not configured or no report yet' }); - return; - } - const httpStatus = report.status === 'unhealthy' ? 503 : 200; - sendJson(res, httpStatus, report); - return; - } - - // GET /api/rate-limit/metrics - if (req.method === 'GET' && url.pathname === '/api/rate-limit/metrics') { - if (!rateLimiter) { - sendJson(res, 200, { - totalRequests: 0, - blockedRequests: 0, - allowedRequests: 0, - uniqueClients: 0, - topBlockedClients: [], - startTime: new Date().toISOString(), - }); - return; - } - sendJson(res, 200, rateLimiter.getMetrics()); - return; - } - - // POST /api/webhooks - if (req.method === 'POST' && url.pathname === '/api/webhooks') { - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - const idempotencyKey = IdempotencyKeyService.extractKey(req.headers) ?? undefined; - - const writeAuthFailure = (statusCode: number, message: string, code: string): void => { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, error: { code, message }, code })); - }; - - collectRawBody(req) - .then(async (rawBody) => { - const sourceIp = - (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || - (req.socket?.remoteAddress as string | undefined); - - const secrets = options.webhookSecrets ?? []; - const maxAgeSeconds = options.signatureExpirationSeconds ?? 300; - const secrets = options.webhookSecrets ?? []; - const maxAgeSeconds = options.signatureExpirationSeconds ?? 300; - - // Use default database for audit log or mock one if not available. - // Actually since SecurityAuditService requires Database, let's pass a real one. - const db = getDatabase(); - const { SecurityAuditService } = require('../services/security-audit'); - const auditService = new SecurityAuditService(db); - - const auth = await verifyWebhookRequest({ - headers: req.headers as Record, - rawBody, - secrets, - sourceIp, - requestId, - correlationId, - maxAgeSeconds, - requireTimestamp: options.requireWebhookTimestamp !== false, - replayCache: webhookReplayCache, - auditService, - }); - - const auth = verifyWebhookRequest({ - headers: req.headers as Record, - rawBody, - secrets, - sourceIp, - requestId, - correlationId, - maxAgeSeconds, - }); - - if (!auth.authenticated) { - logger.warn('Webhook authentication failed', { - requestId, - correlationId, - code: auth.errorCode, - }); - writeAuthFailure(auth.statusCode, auth.message, auth.errorCode); - return; - } - - try { - const acceptWebhook = async (): Promise<{ status: string; verified: boolean }> => { - logger.info('Webhook received and signature verified', { - requestId, - correlationId, - keyId: auth.keyId, - timestampVerified: auth.timestampVerified, - sourceIp, - contentLength: rawBody.length, - idempotencyKey, - }); - return { status: 'accepted', verified: true }; - }; - - if (options.idempotencyService && idempotencyKey) { - const outcome = await options.idempotencyService.processWithIdempotency( - idempotencyKey, - rawBody, - acceptWebhook, - { requestId, correlationId }, - ); - const statusCode = outcome.isDuplicate ? 200 : 202; - res.writeHead(statusCode, { - 'Content-Type': 'application/json', - 'X-Idempotent-Replay': outcome.isDuplicate ? 'true' : 'false', - }); - res.end( - JSON.stringify({ - ...(outcome.result as object), - replay: outcome.isDuplicate, - }), - ); - } else { - const result = await acceptWebhook(); - res.writeHead(202, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(result)); - } - } catch (err) { - if (err instanceof IdempotencyKeyReuseError) { - logger.warn('Webhook rejected: idempotency key reused with different body', { - requestId, - correlationId, - idempotencyKey, - }); - writeAuthFailure(err.statusCode, err.message, err.code); - return; - } - logger.error('Failed to process webhook', { requestId, correlationId, error: err }); - sendErr(res, 500, 'Internal server error', ErrorCode.INTERNAL_ERROR); - } - }) - .catch((err) => { - logger.error('Failed to read webhook body', { - requestId, - correlationId, - error: err instanceof Error ? err.message : String(err), - }); - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ error: 'Failed to read request body', code: 'BODY_READ_FAILED' }), - ); - }); - return; - } - - // POST /api/notifications/validate-batch - if (req.method === 'POST' && url.pathname === '/api/notifications/validate-batch') { - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - let body = ''; - req.on('data', (chunk) => { - body += chunk.toString(); - }); - req.on('end', () => { - try { - const data = JSON.parse(body || 'null'); - const batch = Array.isArray(data) ? data : data?.notifications; - const validator = new BatchValidationService(); - const result = validator.validate(batch); - - if (!result.valid) { - sendOk(res, 400, result); - logger.warn('Batch validation rejected', { - requestId, - correlationId, - errorCount: result.errors.length, - }); - return; - } - - sendOk(res, 200, result); - logger.info('Batch validation passed', { - requestId, - correlationId, - processedCount: result.processedCount, - }); - } catch (error) { - logger.error('Failed to validate notification batch', { - error, - requestId, - correlationId, - }); - sendOk(res, 400, { - valid: false, - processedCount: 0, - errors: [ - { index: -1, code: 'PARSE_ERROR', message: 'Request body must be valid JSON.' }, - ], - }); - } - }); - return; - } - - // POST /api/notifications/import — bulk import from JSON or CSV - if (req.method === 'POST' && url.pathname === '/api/notifications/import') { - // Authenticate before revealing anything about service availability. - if (!(await requireApiKey(['notifications:write']))) return; - - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - if (!validateContentType(req, res, ['application/json', 'text/csv', 'application/csv'])) { - return; - } - - let body = ''; - req.on('data', (chunk) => { - body += chunk.toString(); - }); - req.on('end', async () => { - try { - const contentType = req.headers['content-type'] || ''; - const importer = new NotificationImportService(options.notificationAPI!); - const summary = await importer.importFromBody(body, contentType, { requestId }); - sendOk(res, 200, summary); - logger.info('Bulk notification import finished', { - requestId, - correlationId, - imported: summary.imported, - skipped: summary.skipped, - }); - } catch (error) { - logger.error('Failed to import notifications', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - } - }); - return; - } - - // GET & POST /api/admin/export (and /api/export) — Data Export Utility (#850) - if ( - (req.method === 'GET' || req.method === 'POST') && - (url.pathname === '/api/admin/export' || url.pathname === '/api/export') - ) { - if (!(await requireApiKey(['events:read', 'notifications:read']))) return; - - const processExport = async (rawFilters: Record) => { - try { - const exportService = - options.dataExportService ?? new DataExportService(getDatabase()); - - const validation = validatePayload(rawFilters, Schemas.dataExport); - if (!validation.valid) { - sendErr( - res, - 400, - `Validation failed: ${validation.issues[0]?.message}`, - ErrorCode.BAD_REQUEST, - validation.issues - ); - return; - } - - const type = (rawFilters.type as 'notifications' | 'events' | 'all') || 'all'; - const format = (rawFilters.format as 'json' | 'csv') || 'json'; - const includeSensitive = - rawFilters.includeSensitive === true || rawFilters.includeSensitive === 'true'; - - const limit = rawFilters.limit ? Number(rawFilters.limit) : undefined; - const offset = rawFilters.offset ? Number(rawFilters.offset) : undefined; - - const result = await exportService.exportData({ - type, - format, - includeSensitive, - notificationFilters: { - status: rawFilters.status as string | undefined, - notificationType: (rawFilters.channel || rawFilters.notificationType) as string | undefined, - targetRecipient: (rawFilters.recipient || rawFilters.targetRecipient) as string | undefined, - contractAddress: (rawFilters.contract || rawFilters.contractAddress) as string | undefined, - fromDate: (rawFilters.from || rawFilters.fromDate) as string | undefined, - toDate: (rawFilters.to || rawFilters.toDate) as string | undefined, - limit, - offset, - }, - eventFilters: { - status: rawFilters.status as string | undefined, - eventType: rawFilters.eventType as string | undefined, - contractAddress: (rawFilters.contract || rawFilters.contractAddress) as string | undefined, - fromDate: (rawFilters.from || rawFilters.fromDate) as string | undefined, - toDate: (rawFilters.to || rawFilters.toDate) as string | undefined, - limit, - offset, - }, - }); - - if (format === 'csv') { - res.writeHead(200, { - 'Content-Type': 'text/csv', - 'Content-Disposition': 'attachment; filename="notifychain-export.csv"', - }); - res.end(result.csvContent || ''); - return; - } - - sendOk(res, 200, result); - } catch (error) { - logger.error('Failed to export data', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - } - }; - - if (req.method === 'GET') { - const queryParams: Record = {}; - url.searchParams.forEach((val, key) => { - queryParams[key] = val; - }); - await processExport(queryParams); - return; - } else { - let body = ''; - req.on('data', (chunk) => { body += chunk.toString(); }); - req.on('end', async () => { - try { - const bodyParams = body ? JSON.parse(body) : {}; - await processExport(bodyParams); - } catch (jsonErr) { - sendErr(res, 400, 'Malformed JSON payload in request body', ErrorCode.PARSE_ERROR); - } - }); - return; - } - } - - // POST /api/schedule - if (req.method === 'POST' && url.pathname === '/api/schedule') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - const idempotencyKey = IdempotencyKeyService.extractKey(req.headers) ?? undefined; - let body = ''; - req.on('data', (chunk) => { - body += chunk.toString(); - }); - req.on('end', async () => { - let data: any; - try { - data = JSON.parse(body); - } catch { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - error: 'Request body must be valid JSON.', - code: ErrorCode.PARSE_ERROR, - }), - ); - return; - } - - try { - if (!data.executeAt || !data.payload || !data.targetRecipient) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ - error: 'Missing required fields: executeAt, payload, targetRecipient', - code: 'MISSING_FIELDS', - }), - ); - return; - } - - const executeAt = new Date(data.executeAt); - if (isNaN(executeAt.getTime())) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end( - JSON.stringify({ error: 'executeAt is not a valid date', code: 'INVALID_DATE' }), - ); - return; - } - - const schedule = async (): Promise<{ id: number }> => { - const notificationId = await options.notificationAPI!.scheduleNotification({ - payload: data.payload, - notificationType: data.notificationType || NotificationType.DISCORD, - targetRecipient: data.targetRecipient, - executeAt, - expiresAt: data.expiresAt, - maxRetries: data.maxRetries, - priority: data.priority, - eventId: data.eventId, - contractAddress: data.contractAddress, - metadata: data.metadata, - }); - logger.info('Notification scheduled via API', { - requestId, - correlationId, - notificationId, - executeAt: data.executeAt, - }); - return { id: notificationId }; - }; - - if (options.idempotencyService && idempotencyKey) { - const outcome = await options.idempotencyService.processWithIdempotency( - idempotencyKey, - data, - schedule, - { requestId, correlationId }, - ); - const statusCode = outcome.isDuplicate ? 200 : 201; - res.writeHead(statusCode, { - 'Content-Type': 'application/json', - 'X-Idempotent-Replay': outcome.isDuplicate ? 'true' : 'false', - }); - res.end( - JSON.stringify({ - id: (outcome.result as { id: number }).id, - replay: outcome.isDuplicate, - }), - ); - return; - } - - const { id } = await schedule(); - res.writeHead(201, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ id })); - } catch (error) { - if (error instanceof IdempotencyKeyReuseError) { - logger.warn('Schedule API rejected request: idempotency key body mismatch', { - requestId, - correlationId, - idempotencyKey, - }); - res.writeHead(error.statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: error.message, code: error.code })); - return; - } - - const anyError = error as any; - if (anyError?.name === 'PayloadTooLargeError') { - logger.warn('Payload too large', { - error, - requestId, - correlationId, - payloadSizeBytes: anyError.payloadSizeBytes, - maxSizeBytes: anyError.maxSizeBytes, - }); - res.writeHead(413, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: anyError.message, code: 'PAYLOAD_TOO_LARGE' })); - return; - } - - logger.error('Failed to schedule notification', { - error: error instanceof Error ? error.message : String(error), - requestId, - correlationId, - }); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: (error as Error).message, code: 'SCHEDULE_FAILED' })); - } - }); - return; - } - - // GET /api/schedule/stats - if (req.method === 'GET' && url.pathname === '/api/schedule/stats') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI - .getStatistics() - .then((stats) => { - sendOkWithETag(req, res, stats); - }) - .catch((error) => { - logger.error('Failed to get scheduler stats', { error, requestId, correlationId }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/schedule/execution-metrics - if (req.method === 'GET' && url.pathname === '/api/schedule/execution-metrics') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - (options.notificationAPI as any) - .getExecutionMetrics() - .then((metrics: unknown) => { - sendOkWithETag(req, res, metrics); - }) - .catch((error: Error) => { - logger.error('Failed to get execution metrics', { error, requestId, correlationId }); - sendErr(res, 500, error.message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/schedule/retry-distribution - if (req.method === 'GET' && url.pathname === '/api/schedule/retry-distribution') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - (options.notificationAPI as any) - .getRetryDistribution() - .then((distribution: unknown) => { - sendOkWithETag(req, res, distribution); - }) - .catch((error: Error) => { - logger.error('Failed to get retry distribution', { error, requestId, correlationId }); - sendErr(res, 500, error.message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/schedule/jobs — background job monitoring snapshot - if (req.method === 'GET' && url.pathname === '/api/schedule/jobs') { - const monitor = getJobMonitor(); - const limitParam = url.searchParams.get('limit'); - const limit = limitParam ? Math.min(Math.max(parseInt(limitParam, 10) || 25, 1), 200) : 25; - sendOk(res, 200, { - ...monitor.getSnapshot(), - recentJobs: monitor.listRecentJobs(limit), - recentFailures: monitor.listFailures(limit), - }); - return; - } - - // GET /api/schedule/jobs/failures — failed job log - if (req.method === 'GET' && url.pathname === '/api/schedule/jobs/failures') { - const monitor = getJobMonitor(); - const limitParam = url.searchParams.get('limit'); - const limit = limitParam ? Math.min(Math.max(parseInt(limitParam, 10) || 50, 1), 200) : 50; - sendOk(res, 200, { - failures: monitor.listFailures(limit), - count: monitor.listFailures(limit).length, - }); - return; - } - - // GET /api/schedule/retry-statistics - if (req.method === 'GET' && url.pathname === '/api/schedule/retry-statistics') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI - .getRetryStatistics() - .then((stats) => { - sendOkWithETag(req, res, stats); - }) - .catch((error) => { - logger.error('Failed to get retry statistics', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } - - // GET /api/schedule/queue — queue visibility: list pending jobs - // Returns jobs currently waiting in the queue with id, type, enqueued - // time (createdAt), scheduled delivery time (executeAt), priority, and - // retryCount. Accepts an optional ?limit= query param (default 100). - if (req.method === 'GET' && url.pathname === '/api/schedule/queue') { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const limitParam = url.searchParams.get('limit'); - const limit = limitParam - ? Math.max(1, Math.min(500, parseInt(limitParam, 10) || 100)) - : undefined; - - options.notificationAPI - .getPendingJobs(limit) - .then((jobs) => { - logger.info('Handling GET /api/schedule/queue', { - requestId, - correlationId, - count: jobs.length, - durationMs: Date.now() - startTime, - }); - sendOk(res, 200, { count: jobs.length, jobs }); - }) - .catch((error) => { - logger.error('Failed to get pending jobs', { error, requestId, correlationId }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } - - // GET /api/schedule/queue/metrics — operational metrics for notification queue activity (#797) - if ( - req.method === 'GET' && - (url.pathname === '/api/schedule/queue/metrics' || - url.pathname === '/api/schedule/queue-metrics' || - url.pathname === '/api/notifications/queue-metrics') - ) { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - options.notificationAPI - .getQueueOperationalMetrics() - .then((metrics) => { - logger.info('Handling GET /api/schedule/queue/metrics', { - requestId, - correlationId, - durationMs: Date.now() - startTime, - }); - sendOk(res, 200, metrics); - }) - .catch((error) => { - logger.error('Failed to get queue operational metrics', { - error, - requestId, - correlationId, - }); - handleApiError(res, error, requestId, correlationId); - }); - return; - } - - // GET /api/schedule/:id - if (req.method === 'GET' && url.pathname.startsWith('/api/schedule/')) { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const id = parseInt(url.pathname.split('/').pop() || '', 10); - if (isNaN(id)) { - sendErr(res, 400, 'Invalid notification ID', ErrorCode.BAD_REQUEST); - return; - } - - options.notificationAPI - .getNotification(id) - .then((notification) => { - if (!notification) { - sendErr(res, 404, 'Notification not found', ErrorCode.NOT_FOUND); - return; - } - sendOk(res, 200, notification); - }) - .catch((error) => { - logger.error('Failed to get notification', { error, requestId, correlationId, id }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // POST /api/notifications/:id/cancel - const cancelMatch = url.pathname.match(/^\/api\/notifications\/([^/]+)\/cancel$/); - if (req.method === 'POST' && cancelMatch) { - if (!options.notificationAPI) { - sendErr(res, 503, 'Scheduler not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const id = parseInt(cancelMatch[1], 10); - if (isNaN(id)) { - sendErr(res, 400, 'Invalid notification ID', ErrorCode.BAD_REQUEST); - return; - } - - let body = ''; - req.on('data', (chunk) => { body += chunk; }); - req.on('end', () => { - void (async () => { - try { - const parsed = body ? JSON.parse(body) as { reason?: string } : {}; - const ok = await options.notificationAPI!.cancelNotification(id, parsed.reason, requestId); - - if (!ok) { - sendErr(res, 400, 'Unable to cancel notification', ErrorCode.BAD_REQUEST); - return; - } - - const notification = await options.notificationAPI!.getNotification(id); - sendOk(res, 200, notification); - } catch (error) { - if (error instanceof SyntaxError) { - sendErr(res, 400, 'Invalid JSON', ErrorCode.PARSE_ERROR); - return; - } - logger.error('Failed to cancel notification', { error, requestId, correlationId, id }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - } - })(); - }); - return; - } - - // Get delivery receipts for a scheduled notification - const receiptPath = url.pathname.match(/^\/api\/notifications\/(\d+)\/receipts$/); - if (req.method === 'GET' && receiptPath) { - if (!(await requireApiKey(['notifications:read']))) return; - - if (!options.deliveryReceiptRepository) { - sendErr(res, 503, 'Delivery receipt storage is not configured', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const rawStatus = url.searchParams.get('status'); - const statuses: DeliveryReceiptStatus[] = ['delivered', 'failed', 'rejected', 'pending']; - if (rawStatus && !statuses.includes(rawStatus as DeliveryReceiptStatus)) { - sendErr(res, 400, 'Invalid delivery receipt status', ErrorCode.BAD_REQUEST); - return; - } - - options.deliveryReceiptRepository - .findByNotificationId(Number(receiptPath[1]), rawStatus as DeliveryReceiptStatus | undefined) - .then((receipts) => sendOk(res, 200, { receipts })) - .catch((error) => { - logger.error('Failed to retrieve delivery receipts', { error, requestId, correlationId }); - sendErr(res, 500, 'Failed to retrieve delivery receipts', ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // Get notification delivery history endpoint. - // Matched on the rewritten pathname so /api/v1/notifications/history is - // routed (and authenticated) the same as the unversioned path. - if (req.method === 'GET' && url.pathname === '/api/notifications/history') { - if (!(await requireApiKey(['notifications:read']))) return; - const limit = url.searchParams.get('limit') ? parseInt(url.searchParams.get('limit')!, 10) : undefined; - const offset = url.searchParams.get('offset') ? parseInt(url.searchParams.get('offset')!, 10) : undefined; - const cursor = url.searchParams.get('cursor') || undefined; - const status = url.searchParams.get('status') as 'SUCCESS' | 'FAILED' | 'RETRY' | null; - const startDate = url.searchParams.get('startDate'); - const endDate = url.searchParams.get('endDate'); - - logger.info('Handling GET /api/notifications/history', { - requestId, - correlationId, - limit, - offset, - cursor, - status, - startDate, - endDate, - }); - - historyService - .getHistory({ - limit, - offset, - cursor, - status: status || undefined, - startDate: startDate || undefined, - endDate: endDate || undefined, - }) - .then((result) => { - sendJson(res, 200, result); - logger.info('GET /api/notifications/history complete', { - requestId, - correlationId, - total: result.total, - durationMs: Date.now() - startTime, - }); - }) - .catch((error) => { - logger.error('Failed to retrieve notification history', { - error, - requestId, - correlationId, - }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/notifications/search - if (req.method === 'GET' && url.pathname === '/api/notifications/search') { - const q = url.searchParams.get('q') ?? undefined; - const sender = url.searchParams.get('sender') ?? undefined; - const txHash = url.searchParams.get('txHash') ?? undefined; - const eventId = url.searchParams.get('eventId') ?? undefined; - const status = url.searchParams.get('status') ?? undefined; - const type = url.searchParams.get('type') ?? undefined; - const startDate = url.searchParams.get('startDate') ?? undefined; - const endDate = url.searchParams.get('endDate') ?? undefined; - const limit = url.searchParams.get('limit') - ? parseInt(url.searchParams.get('limit')!, 10) - : undefined; - const offset = url.searchParams.get('offset') - ? parseInt(url.searchParams.get('offset')!, 10) - : undefined; - const rawSortBy = url.searchParams.get('sortBy') ?? undefined; - const sortBy = rawSortBy === 'oldest' || rawSortBy === 'status' ? rawSortBy : 'newest'; - - logger.info('Handling GET /api/notifications/search', { - requestId, - correlationId, - q, - sender, - txHash, - eventId, - status, - type, - startDate, - endDate, - limit, - offset, - sortBy, - }); - - notificationSearchService - .search({ - q, - sender, - txHash, - eventId, - status, - type, - startDate, - endDate, - limit, - offset, - sortBy, - }) - .then((result) => { - sendOk(res, 200, result); - logger.info('GET /api/notifications/search complete', { - requestId, - total: result.total, - durationMs: Date.now() - startTime, - }); - }) - .catch((error) => { - logger.error('Failed to search notifications', { error, requestId, correlationId }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/search/suggestions - if (req.method === 'GET' && url.pathname === '/api/search/suggestions') { - const q = url.searchParams.get('q') || ''; - const limit = url.searchParams.get('limit') - ? parseInt(url.searchParams.get('limit')!, 10) - : undefined; - - logger.info('Handling GET /api/search/suggestions', { requestId, correlationId, q, limit }); - - suggestionService - .getSuggestions(q, limit) - .then((result) => { - sendOk(res, 200, result); - logger.info('GET /api/search/suggestions complete', { - requestId, - correlationId, - durationMs: Date.now() - startTime, - }); - }) - .catch((error) => { - logger.error('Failed to retrieve search suggestions', { - error, - requestId, - correlationId, - }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/templates - if (req.method === 'GET' && url.pathname === '/api/templates') { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - logger.info('Handling GET /api/templates', { requestId, correlationId }); - (options.templateService as any) - .listAll() - .then((templates: any[]) => { - sendOk(res, 200, templates.map(serializeTemplate)); - }) - .catch((error: Error) => { - logger.error('Failed to list templates', { error, requestId, correlationId }); - sendErr(res, 500, error.message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/templates/:id/audit - const templateAuditMatch = url.pathname.match(/^\/api\/templates\/([^/]+)\/audit$/); - if (req.method === 'GET' && templateAuditMatch) { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const templateId = decodeURIComponent(templateAuditMatch[1]); - logger.info('Handling GET /api/templates/:id/audit', { - requestId, - correlationId, - templateId, - }); - - (options.templateService as any) - .getAuditHistory(templateId) - .then(async (records: any[]) => { - const template = await (options.templateService as any).getById(templateId); - if (!template && records.length === 0) { - sendErr(res, 404, 'Template not found', ErrorCode.NOT_FOUND); - return; - } - sendOk(res, 200, { templateId, records: records.map(serializeAuditRecord) }); - }) - .catch((error: Error) => { - logger.error('Failed to load template audit history', { - error, - requestId, - correlationId, - templateId, - }); - sendErr(res, 500, error.message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // GET /api/templates/:id - const getTemplateMatch = url.pathname.match(/^\/api\/templates\/([^/]+)$/); - if (req.method === 'GET' && getTemplateMatch) { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const templateId = decodeURIComponent(getTemplateMatch[1]); - logger.info('Handling GET /api/templates/:id', { requestId, correlationId, templateId }); - - (options.templateService as any) - .getById(templateId) - .then((template: any) => { - if (!template) { - sendErr(res, 404, 'Template not found', ErrorCode.NOT_FOUND); - return; - } - sendOk(res, 200, serializeTemplate(template)); - }) - .catch((error: Error) => { - logger.error('Failed to load template', { error, requestId, correlationId, templateId }); - sendErr(res, 500, error.message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // PUT /api/templates/:id - if (req.method === 'PUT' && getTemplateMatch) { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - const templateId = decodeURIComponent(getTemplateMatch[1]); - const actor = resolveRequestActor(req); - logger.info('Handling PUT /api/templates/:id', { - requestId, - correlationId, - templateId, - actor, - }); - - let body = ''; - req.on('data', (chunk) => { - body += chunk; - }); - req.on('end', () => { - void (async () => { - try { - const parsed = JSON.parse(body) as unknown; - const input = parseTemplateUpdateBody(parsed); - const updated = await (options.templateService as any).update(templateId, input, actor); - logger.info('PUT /api/templates/:id complete', { - requestId, - correlationId, - templateId, - actor, - durationMs: Date.now() - startTime, - }); - sendOk(res, 200, serializeTemplate(updated)); - } catch (error) { - if (error instanceof SyntaxError) { - sendErr(res, 400, 'Invalid JSON', ErrorCode.PARSE_ERROR); - return; - } - if (error instanceof TemplateNotFoundError) { - sendErr(res, 404, error.message, ErrorCode.NOT_FOUND); - return; - } - if ( - error instanceof TemplateValidationError || - (error instanceof Error && error.message.startsWith('Invalid body')) - ) { - sendErr(res, 400, (error as Error).message, ErrorCode.BAD_REQUEST); - return; - } - logger.error('Failed to update template', { - error, - requestId, - correlationId, - templateId, - actor, - }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - } - })(); - }); - return; - } - - // DELETE /api/templates/:id - const deleteTemplateMatch = url.pathname.match(/^\/api\/templates\/([^/]+)$/); - if (req.method === 'DELETE' && deleteTemplateMatch) { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - const templateId = decodeURIComponent(deleteTemplateMatch[1]); - logger.info('Handling DELETE /api/templates/:id', { requestId, correlationId, templateId }); - - (options.templateService as any) - .delete(templateId) - .then(() => { - sendOk(res, 200, { deleted: true }); - }) - .catch((error: any) => { - if (error instanceof TemplateNotFoundError) { - sendErr(res, 404, error.message, ErrorCode.NOT_FOUND); - return; - } - logger.error('Failed to delete template', { - error, - requestId, - correlationId, - templateId, - }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - }); - return; - } - - // POST /api/templates - if (req.method === 'POST' && url.pathname === '/api/templates') { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - logger.info('Handling POST /api/templates', { requestId, correlationId }); - let body = ''; - req.on('data', (chunk) => { - body += chunk; - }); - req.on('end', () => { - void (async () => { - try { - const parsed = JSON.parse(body) as CreateNotificationTemplateInput; - if (!parsed?.id || !parsed?.name || !parsed?.type || !parsed?.body) { - sendErr( - res, - 400, - 'Invalid body: id, name, type, and body are required', - ErrorCode.BAD_REQUEST, - ); - return; - } - - const created = await (options.templateService as any).create(parsed); - sendOk(res, 201, serializeTemplate(created)); - } catch (error) { - if (error instanceof SyntaxError) { - sendErr(res, 400, 'Invalid JSON', ErrorCode.PARSE_ERROR); - return; - } - if (error instanceof TemplateValidationError) { - sendErr(res, 400, error.message, ErrorCode.BAD_REQUEST); - return; - } - logger.error('Failed to create template', { error, requestId, correlationId }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - } - })(); - }); - return; - } - - // POST /api/templates/:id/render - const templateRenderMatch = url.pathname.match(/^\/api\/templates\/([^/]+)\/render$/); - if (req.method === 'POST' && templateRenderMatch) { - if (!options.templateService) { - sendErr(res, 503, 'Template service not enabled', ErrorCode.SERVICE_UNAVAILABLE); - return; - } - - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - const templateId = decodeURIComponent(templateRenderMatch[1]); - logger.info('Handling POST /api/templates/:id/render', { - requestId, - correlationId, - templateId, - }); - - let body = ''; - req.on('data', (chunk) => { - body += chunk; - }); - req.on('end', () => { - void (async () => { - try { - const parsed = body ? (JSON.parse(body) as Record) : {}; - const template = await (options.templateService as any).getById(templateId); - if (!template) { - sendErr(res, 404, `Template not found: ${templateId}`, ErrorCode.NOT_FOUND); - return; - } - const rendered = (options.templateService as any).renderTemplate(template, parsed); - sendOk(res, 200, rendered); - } catch (error) { - if (error instanceof SyntaxError) { - sendErr(res, 400, 'Invalid JSON', ErrorCode.PARSE_ERROR); - return; - } - if (error instanceof TemplateRenderError) { - sendErr(res, 422, error.message, ErrorCode.UNPROCESSABLE); - return; - } - logger.error('Failed to render template', { - error, - requestId, - correlationId, - templateId, - }); - sendErr(res, 500, (error as Error).message, ErrorCode.INTERNAL_ERROR); - } - })(); - }); - return; - } - - // GET /api/preferences/:userId - const getPrefsMatch = url.pathname.match(/^\/api\/preferences\/([^/]+)$/); - if (req.method === 'GET' && getPrefsMatch) { - const userId = decodeURIComponent(getPrefsMatch[1]); - logger.info('Handling GET /api/preferences/:userId', { requestId, correlationId, userId }); - const prefs = preferenceStore.get(userId); - sendOk(res, 200, prefs); - return; - } - - // PUT /api/preferences/:userId - const putPrefsMatch = url.pathname.match(/^\/api\/preferences\/([^/]+)$/); - if (req.method === 'PUT' && putPrefsMatch) { - if (!validateContentType(req, res, ['application/json'])) { - return; - } - - const userId = decodeURIComponent(putPrefsMatch[1]); - logger.info('Handling PUT /api/preferences/:userId', { requestId, correlationId, userId }); - let body = ''; - req.on('data', (chunk) => { - body += chunk; }); req.on('end', () => { try { diff --git a/listener/src/api/notifications-history.test.ts b/listener/src/api/notifications-history.test.ts index 33353a52..37721fc3 100644 --- a/listener/src/api/notifications-history.test.ts +++ b/listener/src/api/notifications-history.test.ts @@ -12,7 +12,7 @@ jest.mock('@stellar/stellar-sdk', () => ({ jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); const BASE_OPTIONS = { diff --git a/listener/src/api/rate-limit-scenarios.test.ts b/listener/src/api/rate-limit-scenarios.test.ts index b337132a..a655a6de 100644 --- a/listener/src/api/rate-limit-scenarios.test.ts +++ b/listener/src/api/rate-limit-scenarios.test.ts @@ -29,6 +29,7 @@ import type { RateLimitConfig } from '../types'; jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/api/rate-limiter.test.ts b/listener/src/api/rate-limiter.test.ts index 651e1975..faf104bf 100644 --- a/listener/src/api/rate-limiter.test.ts +++ b/listener/src/api/rate-limiter.test.ts @@ -20,6 +20,7 @@ import logger from '../utils/logger'; jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/api/request-duration-logger.ts b/listener/src/api/request-duration-logger.ts index 13638d41..05fcca16 100644 --- a/listener/src/api/request-duration-logger.ts +++ b/listener/src/api/request-duration-logger.ts @@ -52,7 +52,7 @@ export function createRequestDurationLogger( const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info'; const message = `HTTP ${req.method} ${sanitizedPath} ${res.statusCode} - ${durationMs.toFixed(2)}ms`; - customLogger.info(message, logData); + customLogger.info(message, { ...logData }); }); next(); diff --git a/listener/src/api/template-routes.ts b/listener/src/api/template-routes.ts index 0a785c21..845fe603 100644 --- a/listener/src/api/template-routes.ts +++ b/listener/src/api/template-routes.ts @@ -50,7 +50,6 @@ async function parseBody(req: http.IncomingMessage): Promise { * Send JSON response */ function sendJson(res: http.ServerResponse, statusCode: number, data: any): void { - res.setHeader('Content-Type', 'application/json'); res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } diff --git a/listener/src/config.ts b/listener/src/config.ts index ff2d4799..0b5ca49c 100644 --- a/listener/src/config.ts +++ b/listener/src/config.ts @@ -1,8 +1,35 @@ -import { Config, ContractConfig, DiscordConfig, WebhookSecret, AppCleanupConfig, EventQueueConfig, RetrySchedulerOptions, AnalyticsConfig, ExpirationConfig, ApiKey, BackfillConfig, LoggingConfig, ApiConfig, EventBatchingConfig, DigestConfig, LocalizationConfig } from './types'; +import { + Config, + ContractConfig, + DiscordConfig, + WebhookSecret, + AppCleanupConfig, + EventQueueConfig, + RetrySchedulerOptions, + RetryPolicyOptions, + AnalyticsConfig, + ExpirationConfig, + ApiKey, + BackfillConfig, + LoggingConfig, + ApiConfig, + RpcRateLimitConfig, + RpcFallbackConfig, + EventBatchingConfig, + DigestConfig, + LocalizationConfig, +} from './types'; import { validateCorsOrigin, CorsValidationError } from './utils/cors-validator'; import { validateSecrets } from './config/validate-secrets'; import { ConfigurationSchemaValidator, APP_CONFIG_SCHEMA } from './config-schema'; import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from './i18n'; +import { + RetryFailureType, + RETRY_FAILURE_TYPES, + DEFAULT_RETRYABLE_FAILURE_TYPES, + parseRetryableFailureTypes, +} from './services/retry-policy'; +import type { CircuitBreakerConfig } from './services/circuit-breaker'; import { SUPPORTED_LOG_FORMATS, SUPPORTED_LOG_LEVELS, @@ -66,1011 +93,4 @@ function loadNotificationDefaultTtlSeconds(): number { } function parseStrictIntegerEnv(name: string, defaultValue: string): number { - const rawValue = trimEnv(name); - if (rawValue !== undefined && !/^-?\d+$/.test(rawValue)) { - throw new ConfigError(`${name} must be a valid integer, got "${rawValue}"`); - } - return parseIntegerEnv(name, defaultValue); -} - -function parseBooleanEnv(name: string, defaultValue: boolean): boolean { - const rawValue = trimEnv(name); - if (rawValue === undefined) return defaultValue; - if (rawValue === 'true') return true; - if (rawValue === 'false') return false; - throw new ConfigError(`${name} must be either "true" or "false", got "${rawValue}"`); -} - -function parseOptionalIntegerEnv(name: string): number | undefined { - const rawValue = trimEnv(name); - return rawValue ? parseStrictIntegerEnv(name, rawValue) : undefined; -} - -function parseJsonEnv(name: string, defaultValue: string): T { - const rawValue = trimEnv(name) ?? defaultValue; - try { - return JSON.parse(rawValue) as T; - } catch { - throw new ConfigError(`${name} must be valid JSON. Received: ${rawValue}`); - } -} - -function parseStringListEnv(name: string): string[] { - const raw = trimEnv(name); - if (!raw) return []; - if (raw.startsWith('[') && raw.endsWith(']')) { - try { - const parsed = JSON.parse(raw); - if (Array.isArray(parsed)) { - return parsed.map((item) => String(item).trim()).filter(Boolean); - } - } catch { - throw new ConfigError(`${name} must be valid JSON array of URL strings. Received: ${raw}`); - } - } - return raw.split(',').map((s) => s.trim()).filter(Boolean); -} - -function validateContractAddresses(value: unknown): ContractConfig[] { - if (!Array.isArray(value)) { - throw new ConfigError('CONTRACT_ADDRESSES must be a JSON array of contract objects.'); - } - - if (value.length === 0) { - throw new ConfigError( - 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. ' + - 'Add contract configurations or the service will not process any events.' - ); - } - - return value.map((item, index) => { - if (typeof item !== 'object' || item === null) { - throw new ConfigError( - `CONTRACT_ADDRESSES[${index}] must be an object with address and events.`, - ); - } - - const address = (item as any).address; - const events = (item as any).events; - - if (typeof address !== 'string' || !address.trim()) { - throw new ConfigError(`CONTRACT_ADDRESSES[${index}].address must be a non-empty string.`); - } - - if (!Array.isArray(events) || events.some((event) => typeof event !== 'string')) { - throw new ConfigError( - `CONTRACT_ADDRESSES[${index}].events must be an array of string event names.`, - ); - } - - return { - address: address.trim(), - events: events.map((event) => event.trim()), - }; - }); -} - -function loadDiscordConfig(): DiscordConfig | undefined { - const webhookUrl = trimEnv('DISCORD_WEBHOOK_URL'); - const webhookId = trimEnv('DISCORD_WEBHOOK_ID'); - - if (!webhookUrl && !webhookId) { - return undefined; - } - - if (!webhookUrl) { - throw new ConfigError('DISCORD_WEBHOOK_URL is required when DISCORD_WEBHOOK_ID is provided.'); - } - - if (!webhookId) { - throw new ConfigError('DISCORD_WEBHOOK_ID is required when DISCORD_WEBHOOK_URL is provided.'); - } - - return { - webhookUrl, - webhookId, - deduplicationWindowMs: parseIntegerEnv('NOTIFICATION_DEDUPLICATION_WINDOW_MS', '60000'), - deduplicationMaxSize: parseIntegerEnv('NOTIFICATION_DEDUPLICATION_MAX_SIZE', '10000'), - }; -} - -function validateWebhookSecrets(value: unknown): WebhookSecret[] { - if (!Array.isArray(value)) { - throw new ConfigError('WEBHOOK_SECRETS must be a JSON array of secret objects.'); - } - - return value.map((item, index) => { - if (typeof item !== 'object' || item === null) { - throw new ConfigError(`WEBHOOK_SECRETS[${index}] must be an object with id and secret.`); - } - - const id = (item as any).id; - const secret = (item as any).secret; - - if (typeof id !== 'string' || !id.trim()) { - throw new ConfigError(`WEBHOOK_SECRETS[${index}].id must be a non-empty string.`); - } - - if (typeof secret !== 'string' || !secret.trim()) { - throw new ConfigError(`WEBHOOK_SECRETS[${index}].secret must be a non-empty string.`); - } - - return { id: id.trim(), secret: secret.trim() }; - }); -} - -function validateApiKeys(value: unknown): ApiKey[] { - if (!Array.isArray(value)) { - throw new ConfigError('API_KEYS must be a JSON array of key objects.'); - } - - return value.map((item, index) => { - if (typeof item !== 'object' || item === null) { - throw new ConfigError(`API_KEYS[${index}] must be an object with key (and optional name).`); - } - - const key = (item as any).key; - const name = (item as any).name; - - if (typeof key !== 'string' || !key.trim()) { - throw new ConfigError(`API_KEYS[${index}].key must be a non-empty string.`); - } - - return { key: key.trim(), name: name?.trim() }; - }); -} - -function loadCleanupConfig(): AppCleanupConfig { - const processedEventRetentionMs = parseIntegerEnv( - 'PROCESSED_EVENT_RETENTION_MS', - String(30 * 24 * 60 * 60 * 1000), - ); - const executionLogRetentionMs = parseIntegerEnv( - 'EXECUTION_LOG_RETENTION_MS', - String(90 * 24 * 60 * 60 * 1000), - ); - const rateLimitEventRetentionMs = parseIntegerEnv( - 'RATE_LIMIT_EVENT_RETENTION_MS', - String(24 * 60 * 60 * 1000), - ); - return { - enabled: parseBooleanEnv('CLEANUP_ENABLED', true), - intervalMs: parseStrictIntegerEnv('CLEANUP_INTERVAL_MS', String(60 * 60 * 1000)), - retentionDays: parseStrictIntegerEnv('CLEANUP_RETENTION_DAYS', '30'), - retentionOverridesMs: { - processedEvents: parseOptionalIntegerEnv('PROCESSED_EVENT_RETENTION_MS'), - executionLogs: parseOptionalIntegerEnv('EXECUTION_LOG_RETENTION_MS'), - rateLimitEvents: parseOptionalIntegerEnv('RATE_LIMIT_EVENT_RETENTION_MS'), - }, - notificationRetentionMs: parseIntegerEnv('NOTIFICATION_RETENTION_MS', String(7 * 24 * 60 * 60 * 1000)), - rateLimitEventRetentionMs, - eventRetentionMs: parseIntegerEnv('EVENT_RETENTION_MS', String(24 * 60 * 60 * 1000)), - processedEventRetentionMs, - executionLogRetentionMs, - }; -} - -function loadAnalyticsConfig(): AnalyticsConfig { - return { - enabled: trimEnv('ANALYTICS_ENABLED') !== 'false', - maxRecords: parseIntegerEnv('ANALYTICS_MAX_RECORDS', '10000'), - maxBuckets: parseIntegerEnv('ANALYTICS_MAX_BUCKETS', '168'), - bucketSizeMs: parseIntegerEnv('ANALYTICS_BUCKET_SIZE_MS', String(60 * 60 * 1000)), - persistIntervalMs: parseIntegerEnv('ANALYTICS_PERSIST_INTERVAL_MS', '300000'), - snapshotRetentionDays: parseIntegerEnv('ANALYTICS_SNAPSHOT_RETENTION_DAYS', '30'), - }; -} - -function loadRetrySchedulerConfig(policy: RetryPolicyOptions): RetrySchedulerOptions { - return { - enabled: trimEnv('RETRY_SCHEDULER_ENABLED') !== 'false', - webhookTimeoutMs: parseIntegerEnv('WEBHOOK_DELIVERY_TIMEOUT_MS', '10000'), - pollIntervalMs: parseIntegerEnv('RETRY_SCHEDULER_POLL_INTERVAL_MS', '15000'), - lockTimeoutMs: parseIntegerEnv('RETRY_SCHEDULER_LOCK_TIMEOUT_MS', '60000'), - processorId: trimEnv('RETRY_SCHEDULER_PROCESSOR_ID'), - batchSize: parseIntegerEnv('RETRY_SCHEDULER_BATCH_SIZE', '10'), - baseDelayMs: parseIntegerEnv('RETRY_BASE_DELAY_MS', '5000'), - multiplier: parseIntegerEnv('RETRY_MULTIPLIER', '2'), - maxDelayMs: parseIntegerEnv('RETRY_MAX_DELAY_MS', String(60 * 60 * 1000)), - jitter: trimEnv('RETRY_JITTER') !== 'false', -// Policy knobs are owned by the retry policy; fold them in so the scheduler - // and the in-memory queues agree on the attempt budget and on which failure - // types are worth retrying. - maxAttempts: policy.maxAttempts, - retryableFailureTypes: policy.retryableFailureTypes, - webhookTimeoutMs: parseIntegerEnv( - 'WEBHOOK_TIMEOUT_MS', - String(DEFAULT_WEBHOOK_TIMEOUT_MS) - ), - }; -} - -/** - * Load the notification retry policy (#842). - * - * The delay curve deliberately reuses `RETRY_BASE_DELAY_MS` / `RETRY_MULTIPLIER` - * / `RETRY_MAX_DELAY_MS` / `RETRY_JITTER`, which the retry scheduler and the - * in-memory retry queue already share, so there is a single knob per concern. - * - * RETRY_POLICY_MAX_ATTEMPTS - hard ceiling on attempts (unset = no ceiling) - * RETRY_POLICY_RETRYABLE_FAILURE_TYPES - comma-separated eligible failure types - */ -function loadRetryPolicyConfig(): RetryPolicyOptions { - const rawMaxAttempts = trimEnv('RETRY_POLICY_MAX_ATTEMPTS'); - const maxAttempts = - rawMaxAttempts === undefined || rawMaxAttempts === '' - ? undefined - : parseIntegerEnv('RETRY_POLICY_MAX_ATTEMPTS', '1'); - - let retryableFailureTypes: RetryFailureType[]; - try { - retryableFailureTypes = - parseRetryableFailureTypes(trimEnv('RETRY_POLICY_RETRYABLE_FAILURE_TYPES')) ?? - [...DEFAULT_RETRYABLE_FAILURE_TYPES]; - } catch (err) { - throw new ConfigError( - `RETRY_POLICY_RETRYABLE_FAILURE_TYPES is invalid: ${ - err instanceof Error ? err.message : String(err) - }`, - ); - } - - return { maxAttempts, retryableFailureTypes }; -} - -function loadExpirationConfig(): ExpirationConfig { - const defaultExpirationMs = parseIntegerEnv('EXPIRATION_DEFAULT_MS', String(24 * 60 * 60 * 1000)); - const perEventTypeExpirationJson = trimEnv('EXPIRATION_PER_EVENT_TYPE'); - let perEventTypeExpiration: Record | undefined; - - if (perEventTypeExpirationJson) { - try { - perEventTypeExpiration = JSON.parse(perEventTypeExpirationJson); - if ( - typeof perEventTypeExpiration !== 'object' || - perEventTypeExpiration === null || - Array.isArray(perEventTypeExpiration) - ) { - throw new ConfigError('EXPIRATION_PER_EVENT_TYPE must be a valid JSON object'); - } - } catch (e) { - if (e instanceof ConfigError) { - throw e; - } - throw new ConfigError( - `EXPIRATION_PER_EVENT_TYPE must be valid JSON. Received: ${perEventTypeExpirationJson}`, - ); - } - } - - return { - defaultExpirationMs, - perEventTypeExpiration, - enabled: trimEnv('EXPIRATION_ENABLED') !== 'false', - }; -} - -/** - * Load backfill safety configuration. - * - * BACKFILL_MAX_LEDGERS controls how many ledgers behind the network tip the - * listener will start from when it has no persisted cursor (cold start or - * after downtime). Setting it to 0 restores the original unlimited behaviour. - * - * Default: 10 000 ledgers (~14 hours at ~5 s/ledger on Stellar). - */ -function loadBackfillConfig(): BackfillConfig { - return { - maxLedgers: parseIntegerEnv('BACKFILL_MAX_LEDGERS', '10000'), - }; -} - -/** - * Load RPC rate limiting configuration for event ingestion. - * - * RPC_RATE_LIMIT_ENABLED controls whether rate limiting is applied to RPC - * requests during event ingestion. This prevents excessive RPC requests and - * resource consumption. - * - * Default: enabled, 10 requests per second, burst of 20, 1s throttle delay. - */ -function loadRpcRateLimitConfig(): RpcRateLimitConfig { - return { - enabled: trimEnv('RPC_RATE_LIMIT_ENABLED') !== 'false', - maxRequestsPerSecond: parseIntegerEnv('RPC_RATE_LIMIT_MAX_REQUESTS_PER_SECOND', '10'), - burstSize: parseIntegerEnv('RPC_RATE_LIMIT_BURST_SIZE', '20'), - throttleDelayMs: parseIntegerEnv('RPC_RATE_LIMIT_THROTTLE_DELAY_MS', '1000'), - }; -} - -function loadRpcFallbackConfig(fallbackUrls: string[]): RpcFallbackConfig { - return { - fallbackUrls, - failureThreshold: parseIntegerEnv('RPC_FAILURE_THRESHOLD', '3'), - cooldownMs: parseIntegerEnv('RPC_COOLDOWN_MS', '60000'), - requestTimeoutMs: parseIntegerEnv('RPC_REQUEST_TIMEOUT_MS', '10000'), - maxRetries: parseOptionalIntegerEnv('RPC_MAX_RETRIES'), - }; -} - -function loadCircuitBreakerConfig(): CircuitBreakerConfig { - return { - failureThreshold: parseIntegerEnv('CIRCUIT_BREAKER_FAILURE_THRESHOLD', '5'), - recoveryTimeoutMs: parseIntegerEnv('CIRCUIT_BREAKER_RECOVERY_TIMEOUT_MS', '60000'), - successThreshold: parseIntegerEnv('CIRCUIT_BREAKER_SUCCESS_THRESHOLD', '2'), - }; -} - -/** - * High-volume event batching (feature: batched delivery) (#batching). - * - * EVENT_BATCHING_ENABLED - master switch; off by default so delivery stays - * one-message-per-event unless an operator opts in. - * EVENT_BATCHING_MAX_SIZE - events per group before an early flush. - * EVENT_BATCHING_WINDOW_MS - longest an event waits before being flushed. - */ -function loadEventBatchingConfig(): EventBatchingConfig { - return { - enabled: parseBooleanEnv('EVENT_BATCHING_ENABLED', false), - maxBatchSize: parseIntegerEnv('EVENT_BATCHING_MAX_SIZE', '50'), - windowMs: parseStrictIntegerEnv('EVENT_BATCHING_WINDOW_MS', '5000'), - }; -} - -/** - * Periodic digest summaries (feature: digest delivery). - * - * DIGEST_ENABLED - master switch; off by default (immediate alerts). - * DIGEST_INTERVAL_MS - digest period; defaults to one hour. - * DIGEST_MAX_EVENTS - events accumulated before an early digest. - */ -function loadDigestConfig(): DigestConfig { - return { - enabled: parseBooleanEnv('DIGEST_ENABLED', false), - intervalMs: parseStrictIntegerEnv('DIGEST_INTERVAL_MS', String(60 * 60 * 1000)), - maxEventsPerDigest: parseIntegerEnv('DIGEST_MAX_EVENTS', '100'), - }; -} - -/** - * Localization settings (feature: template separation). - * - * DEFAULT_LOCALE - initial locale for user-facing text (default: "en"). - * Validated against the supported locales below. - */ -function loadLocalizationConfig(): LocalizationConfig { - return { - defaultLocale: (trimEnv('DEFAULT_LOCALE') || DEFAULT_LOCALE).toLowerCase(), - }; -} - -export function loadConfig(): Config { - validateRequiredEnvVars(); - - const discord = loadDiscordConfig(); - const retryPolicy = loadRetryPolicyConfig(); - const rawContractAddresses = parseJsonEnv('CONTRACT_ADDRESSES', '[]'); - const rawWebhookSecrets = parseJsonEnv('WEBHOOK_SECRETS', '[]'); - const rawApiKeys = parseJsonEnv('API_KEYS', '[]'); - const clientOverrides = parseJsonEnv>( - 'RATE_LIMIT_CLIENT_OVERRIDES', - '{}', - ); - - const explicitRpcUrl = trimEnv('STELLAR_RPC_URL'); - const allRpcUrlsFromEnv = parseStringListEnv('STELLAR_RPC_URLS'); - const explicitFallbacks = parseStringListEnv('STELLAR_RPC_FALLBACK_URLS'); - - const primaryRpcUrl = - explicitRpcUrl || allRpcUrlsFromEnv[0] || 'https://soroban-testnet.stellar.org:443'; - - const combinedFallbacks = Array.from( - new Set([ - ...explicitFallbacks, - ...(allRpcUrlsFromEnv.length > 1 ? allRpcUrlsFromEnv.slice(1) : []), - ]) - ).filter((url) => url !== primaryRpcUrl); - - const rpcFallback = loadRpcFallbackConfig(combinedFallbacks); - const stellarRpcUrls = [primaryRpcUrl, ...combinedFallbacks]; - - return { - stellarNetwork: trimEnv('STELLAR_NETWORK') || 'testnet', - stellarRpcUrl: primaryRpcUrl, - stellarRpcFallbackUrls: combinedFallbacks, - stellarRpcUrls, - rpcFallback, - stellarNetworkPassphrase: trimEnv('STELLAR_NETWORK_PASSPHRASE') || 'Test SDF Network ; September 2015', - contractAddresses: validateContractAddresses(rawContractAddresses), - pollIntervalMs: parseIntegerEnv('POLL_INTERVAL_MS', '30000'), - eventBatchSize: parseIntegerEnv('EVENT_BATCH_SIZE', '100'), - maxReconnectAttempts: parseIntegerEnv('MAX_RECONNECT_ATTEMPTS', '5'), - reconnectDelayMs: parseIntegerEnv('RECONNECT_DELAY_MS', '5000'), - eventsApiPort: parseIntegerEnv('EVENTS_API_PORT', '8787'), - eventsApiCorsOrigin: trimEnv('EVENTS_API_CORS_ORIGIN') || 'http://localhost:5173', - databasePath: trimEnv('DATABASE_PATH') || './data/notifications.db', - discord, - retryQueue: { - backoff: { - initialDelayMs: parseIntegerEnv('RETRY_BASE_DELAY_MS', '5000'), - maxRetries: parseIntegerEnv('RETRY_MAX_RETRIES', '5'), - multiplier: parseIntegerEnv('RETRY_MULTIPLIER', '2'), - jitter: trimEnv('RETRY_JITTER') !== 'false', - maxDelayMs: parseIntegerEnv('RETRY_MAX_DELAY_MS', String(60 * 60 * 1000)), - }, - processIntervalMs: parseIntegerEnv('RETRY_QUEUE_PROCESS_INTERVAL_MS', '5000'), - }, - eventQueue: { - maxConcurrency: parseIntegerEnv('EVENT_QUEUE_MAX_CONCURRENCY', '1'), - maxRetries: parseIntegerEnv('EVENT_QUEUE_MAX_RETRIES', '3'), - baseDelayMs: parseIntegerEnv('EVENT_QUEUE_BASE_DELAY_MS', '2000'), - pollIntervalMs: parseIntegerEnv('EVENT_QUEUE_POLL_INTERVAL_MS', '1000'), - }, - webhookSecrets: validateWebhookSecrets(rawWebhookSecrets), - apiKeys: validateApiKeys(rawApiKeys), - scheduler: { - enabled: trimEnv('SCHEDULER_ENABLED') !== 'false', - pollIntervalMs: parseIntegerEnv('SCHEDULER_POLL_INTERVAL_MS', '10000'), - lockTimeoutMs: parseIntegerEnv('SCHEDULER_LOCK_TIMEOUT_MS', '60000'), - processorId: trimEnv('SCHEDULER_PROCESSOR_ID'), - batchSize: parseIntegerEnv('SCHEDULER_BATCH_SIZE', '10'), - concurrency: parseIntegerEnv('WORKER_CONCURRENCY', '1'), - timingBufferMs: parseIntegerEnv('SCHEDULER_TIMING_BUFFER_MS', '60000'), - }, - retryScheduler: loadRetrySchedulerConfig(retryPolicy), - retryPolicy, - rateLimit: { - enabled: trimEnv('RATE_LIMIT_ENABLED') !== 'false', - windowMs: parseIntegerEnv('RATE_LIMIT_WINDOW_MS', '60000'), - maxRequests: parseIntegerEnv('RATE_LIMIT_MAX_REQUESTS', '60'), - clientOverrides, - }, - cleanup: loadCleanupConfig(), - analytics: loadAnalyticsConfig(), - expiration: loadExpirationConfig(), - notificationDefaultTtlSeconds: loadNotificationDefaultTtlSeconds(), - backfill: loadBackfillConfig(), - rpcRateLimit: loadRpcRateLimitConfig(), - logging: loadLoggingConfig(), - api: loadApiConfig(), - circuitBreaker: loadCircuitBreakerConfig(), - eventBatching: loadEventBatchingConfig(), - digest: loadDigestConfig(), - localization: loadLocalizationConfig(), - }; -} - -/** - * Observability settings. - * - * Raw strings are carried through and validated in `validateConfig`, matching - * how the rest of this loader works: collect everything, then report every - * problem at once rather than throwing on the first bad field. - */ -function loadLoggingConfig(): LoggingConfig { - return { - level: trimEnv('LOG_LEVEL') || 'info', - // Preserves the previous implicit behaviour when LOG_FORMAT is unset: - // JSON in production, human-readable elsewhere. - format: trimEnv('LOG_FORMAT') || (process.env.NODE_ENV === 'production' ? 'json' : 'pretty'), - }; -} - -/** HTTP surface settings. */ -function loadApiConfig(): ApiConfig { - return { - maxBodyBytes: parseIntegerEnv('API_MAX_BODY_BYTES', String(DEFAULT_MAX_BODY_BYTES)), - }; -} - -/** - * Validate a fully-loaded Config object and throw a descriptive ConfigError - * for every invalid value found. Call this immediately after `loadConfig()` - * so that misconfigured services never start processing events (#494). - * - * All violations are collected before throwing so operators see every problem - * in a single error message rather than having to fix-and-restart repeatedly. - */ -export function validateConfig(config: Config): void { - const errors: string[] = []; - - // ── Network ──────────────────────────────────────────────────────────────── - // Validate RPC URL format - if (!config.stellarRpcUrl) { - errors.push('STELLAR_RPC_URL must be a non-empty string.'); - } else { - try { - const url = new URL(config.stellarRpcUrl); - if (url.protocol !== 'http:' && url.protocol !== 'https:') { - errors.push( - `STELLAR_RPC_URL must use HTTP or HTTPS protocol (received: "${url.protocol}").`, - ); - } - } catch { - errors.push(`STELLAR_RPC_URL is not a valid URL (received: "${config.stellarRpcUrl}").`); - } - } - - // Validate Fallback RPC URLs - if (config.stellarRpcFallbackUrls && Array.isArray(config.stellarRpcFallbackUrls)) { - for (const [idx, fbUrl] of config.stellarRpcFallbackUrls.entries()) { - if (!fbUrl || typeof fbUrl !== 'string' || fbUrl.trim() === '') { - errors.push(`Fallback RPC URL at index ${idx} must be a non-empty string.`); - continue; - } - try { - const url = new URL(fbUrl); - if (url.protocol !== 'http:' && url.protocol !== 'https:') { - errors.push( - `Fallback RPC URL at index ${idx} must use HTTP or HTTPS protocol (received: "${url.protocol}").` - ); - } - } catch { - errors.push( - `Fallback RPC URL at index ${idx} is not a valid URL (received: "${fbUrl}").` - ); - } - } - } - - if (config.rpcFallback) { - if (config.rpcFallback.failureThreshold < 1) { - errors.push( - `RPC_FAILURE_THRESHOLD must be >= 1 (received: ${config.rpcFallback.failureThreshold}).` - ); - } - if (config.rpcFallback.cooldownMs < 0) { - errors.push( - `RPC_COOLDOWN_MS must be >= 0 (received: ${config.rpcFallback.cooldownMs}).` - ); - } - if (config.rpcFallback.requestTimeoutMs < 500) { - errors.push( - `RPC_REQUEST_TIMEOUT_MS must be at least 500 ms (received: ${config.rpcFallback.requestTimeoutMs}).` - ); - } - } - - if (!config.stellarNetworkPassphrase || config.stellarNetworkPassphrase.trim() === '') { - errors.push('STELLAR_NETWORK_PASSPHRASE must be a non-empty string.'); - } - - // Validate network passphrase matches common Stellar networks - if (config.stellarNetworkPassphrase) { - const knownPassphrases = [ - 'Test SDF Network ; September 2015', - 'Public Global Stellar Network ; September 2015', - ]; - const isKnown = knownPassphrases.some( - (known) => config.stellarNetworkPassphrase.trim() === known, - ); - if (!isKnown && config.stellarNetwork !== 'standalone') { - errors.push( - `STELLAR_NETWORK_PASSPHRASE does not match known Stellar networks. ` + - `Expected one of: ${knownPassphrases.join(', ')} ` + - `(received: "${config.stellarNetworkPassphrase}").`, - ); - } - } - - // ── Database path ────────────────────────────────────────────────────────── - if (!config.databasePath || config.databasePath.trim() === '') { - errors.push('DATABASE_PATH must be a non-empty string.'); - } - - // ── Polling ──────────────────────────────────────────────────────────────── - if (config.pollIntervalMs < 1000) { - errors.push( - `POLL_INTERVAL_MS must be at least 1000 ms to avoid excessive RPC load ` + - `(received: ${config.pollIntervalMs}).`, - ); - } - - if (config.eventBatchSize < 1) { - errors.push(`EVENT_BATCH_SIZE must be >= 1 (received: ${config.eventBatchSize}).`); - } - - if (config.maxReconnectAttempts < 1) { - errors.push(`MAX_RECONNECT_ATTEMPTS must be >= 1 (received: ${config.maxReconnectAttempts}).`); - } - - if (config.reconnectDelayMs < 0) { - errors.push(`RECONNECT_DELAY_MS must be >= 0 (received: ${config.reconnectDelayMs}).`); - } - - // ── API server ───────────────────────────────────────────────────────────── - if (config.eventsApiPort < 1 || config.eventsApiPort > 65535) { - errors.push(`EVENTS_API_PORT must be between 1 and 65535 (received: ${config.eventsApiPort}).`); - } - - // Validate CORS configuration during startup (#689) - if (config.eventsApiCorsOrigin) { - try { - validateCorsOrigin({ - corsOrigin: config.eventsApiCorsOrigin, - nodeEnv: process.env.NODE_ENV, - }); - } catch (corsErr) { - if (corsErr instanceof CorsValidationError) { - errors.push(`EVENTS_API_CORS_ORIGIN: ${corsErr.message}`); - } else { - errors.push(`EVENTS_API_CORS_ORIGIN invalid: ${String(corsErr)}`); - } - } - } - - // ── Contract addresses ───────────────────────────────────────────────────── - if (!Array.isArray(config.contractAddresses)) { - errors.push('CONTRACT_ADDRESSES must be a JSON array.'); - } else { - if (config.contractAddresses.length === 0) { - errors.push( - 'CONTRACT_ADDRESSES is empty. The listener requires at least one contract to monitor. ' + - 'Add contract configurations or the service will not process any events.', - ); - } - - config.contractAddresses.forEach((contract, index) => { - if (!contract.address || typeof contract.address !== 'string') { - errors.push(`CONTRACT_ADDRESSES[${index}].address must be a non-empty string.`); - } else { - // Validate Stellar contract address format (starts with 'C' and is 56 chars) - const trimmedAddress = contract.address.trim(); - if (trimmedAddress.length !== 56) { - errors.push( - `CONTRACT_ADDRESSES[${index}].address must be exactly 56 characters ` + - `(received: ${trimmedAddress.length} characters).`, - ); - } - if (!trimmedAddress.startsWith('C')) { - errors.push( - `CONTRACT_ADDRESSES[${index}].address must start with 'C' for Stellar contracts ` + - `(received: "${trimmedAddress.substring(0, 1)}").`, - ); - } - } - - if (!Array.isArray(contract.events) || contract.events.length === 0) { - errors.push( - `CONTRACT_ADDRESSES[${index}].events must be a non-empty array of event names.`, - ); - } else { - // Validate event names are not empty - contract.events.forEach((event, eventIndex) => { - if (typeof event !== 'string' || event.trim() === '') { - errors.push( - `CONTRACT_ADDRESSES[${index}].events[${eventIndex}] must be a non-empty string.`, - ); - } - }); - } - }); - } - - // ── Discord ──────────────────────────────────────────────────────────────── - if (config.discord) { - if (!config.discord.webhookUrl.startsWith('https://discord.com/api/webhooks/')) { - errors.push( - 'DISCORD_WEBHOOK_URL must start with "https://discord.com/api/webhooks/". ' + - 'Verify the URL copied from Discord server settings.', - ); - } - if (!config.discord.webhookId || config.discord.webhookId.trim() === '') { - errors.push('DISCORD_WEBHOOK_ID must be a non-empty string.'); - } - if ( - config.discord.deduplicationWindowMs !== undefined && - config.discord.deduplicationWindowMs < 0 - ) { - errors.push( - `NOTIFICATION_DEDUPLICATION_WINDOW_MS must be >= 0 ` + - `(received: ${config.discord.deduplicationWindowMs}).`, - ); - } - } - - // ── Scheduler ───────────────────────────────────────────────────────────── - if (config.scheduler) { - if (config.scheduler.pollIntervalMs < 1000) { - errors.push( - `SCHEDULER_POLL_INTERVAL_MS must be >= 1000 ms ` + - `(received: ${config.scheduler.pollIntervalMs}).`, - ); - } - if (config.scheduler.lockTimeoutMs < config.scheduler.pollIntervalMs) { - errors.push( - 'SCHEDULER_LOCK_TIMEOUT_MS must be >= SCHEDULER_POLL_INTERVAL_MS to avoid ' + - 'premature lock expiry. Increase SCHEDULER_LOCK_TIMEOUT_MS or reduce ' + - 'SCHEDULER_POLL_INTERVAL_MS.', - ); - } - if (config.scheduler.concurrency < 1) { - errors.push( - `WORKER_CONCURRENCY must be >= 1 (received: ${config.scheduler.concurrency}). ` + - 'Set the number of notifications processed concurrently per poll cycle.' - ); - } - if (config.scheduler.batchSize < 1) { - errors.push(`SCHEDULER_BATCH_SIZE must be >= 1 (received: ${config.scheduler.batchSize}).`); - } - } - - // ── Retry scheduler ──────────────────────────────────────────────────────── - if (config.retryScheduler) { - if (config.retryScheduler.pollIntervalMs < 1000) { - errors.push( - `RETRY_SCHEDULER_POLL_INTERVAL_MS must be >= 1000 ms ` + - `(received: ${config.retryScheduler.pollIntervalMs}).`, - ); - } - if (config.retryScheduler.baseDelayMs < 0) { - errors.push( - `RETRY_BASE_DELAY_MS must be >= 0 (received: ${config.retryScheduler.baseDelayMs}).`, - ); - } - if (config.retryScheduler.multiplier < 1) { - errors.push( - `RETRY_MULTIPLIER must be >= 1 (received: ${config.retryScheduler.multiplier}). ` + - 'A multiplier below 1 would cause retry delays to shrink, not grow.', - ); - } - if (config.retryScheduler.maxDelayMs < config.retryScheduler.baseDelayMs) { - errors.push( - 'RETRY_MAX_DELAY_MS must be >= RETRY_BASE_DELAY_MS. ' + - `Received max=${config.retryScheduler.maxDelayMs}, base=${config.retryScheduler.baseDelayMs}.`, - ); - } - if (config.retryScheduler.batchSize < 1) { - errors.push( - `RETRY_SCHEDULER_BATCH_SIZE must be >= 1 (received: ${config.retryScheduler.batchSize}).`, - ); - } - if (config.retryScheduler.webhookTimeoutMs < 1) { - errors.push( - `WEBHOOK_TIMEOUT_MS must be >= 1 ms (received: ${config.retryScheduler.webhookTimeoutMs}).`, - ); - } else if (config.retryScheduler.webhookTimeoutMs > MAX_WEBHOOK_TIMEOUT_MS) { - errors.push( - `WEBHOOK_TIMEOUT_MS must be <= ${MAX_WEBHOOK_TIMEOUT_MS} ms ` + - `(received: ${config.retryScheduler.webhookTimeoutMs}).`, - ); - } - } - - // ── Retry policy (#842) ─────────────────────────────────────────────────── - if (config.retryPolicy) { - if ( - config.retryPolicy.maxAttempts !== undefined && - config.retryPolicy.maxAttempts < 1 - ) { - errors.push( - `RETRY_POLICY_MAX_ATTEMPTS must be >= 1 (received: ${config.retryPolicy.maxAttempts}). ` + - 'A value of 1 disables retries entirely.', - ); - } - if (!Array.isArray(config.retryPolicy.retryableFailureTypes)) { - errors.push('RETRY_POLICY_RETRYABLE_FAILURE_TYPES must be a list of failure types.'); - } else if (config.retryPolicy.retryableFailureTypes.length === 0) { - errors.push( - 'RETRY_POLICY_RETRYABLE_FAILURE_TYPES must list at least one failure type. ' + - 'Omit the variable entirely to use the default transient set.', - ); - } else { - const unknownTypes = config.retryPolicy.retryableFailureTypes.filter( - (type) => !RETRY_FAILURE_TYPES.includes(type), - ); - if (unknownTypes.length > 0) { - errors.push( - `RETRY_POLICY_RETRYABLE_FAILURE_TYPES contains unknown failure type(s): ` + - `${unknownTypes.join(', ')}. Supported values: ${RETRY_FAILURE_TYPES.join(', ')}.`, - ); - } - } - } - - // ── Rate limiting ────────────────────────────────────────────────────────── - if (config.rateLimit) { - if (config.rateLimit.windowMs < 1000) { - errors.push( - `RATE_LIMIT_WINDOW_MS must be >= 1000 ms (received: ${config.rateLimit.windowMs}).`, - ); - } - if (config.rateLimit.maxRequests < 1) { - errors.push( - `RATE_LIMIT_MAX_REQUESTS must be >= 1 (received: ${config.rateLimit.maxRequests}).`, - ); - } - } - - // ── Analytics ───────────────────────────────────────────────────────────── - if (config.analytics) { - if (config.analytics.maxRecords < 1) { - errors.push(`ANALYTICS_MAX_RECORDS must be >= 1 (received: ${config.analytics.maxRecords}).`); - } - if (config.analytics.bucketSizeMs < 60_000) { - errors.push( - `ANALYTICS_BUCKET_SIZE_MS must be >= 60000 ms (1 minute) ` + - `(received: ${config.analytics.bucketSizeMs}).`, - ); - } - if (config.analytics.snapshotRetentionDays < 1) { - errors.push( - `ANALYTICS_SNAPSHOT_RETENTION_DAYS must be >= 1 ` + - `(received: ${config.analytics.snapshotRetentionDays}).`, - ); - } - } - - // ── Cleanup ──────────────────────────────────────────────────────────────── - if (config.cleanup) { - if (config.cleanup.intervalMs < 60_000) { - errors.push( - `CLEANUP_INTERVAL_MS must be >= 60000 ms (1 minute) ` + - `(received: ${config.cleanup.intervalMs}).`, - ); - } - if (config.cleanup.notificationRetentionMs < 60_000) { - errors.push( - `NOTIFICATION_RETENTION_MS must be >= 60000 ms ` + - `(received: ${config.cleanup.notificationRetentionMs}).`, - ); - } - if (config.cleanup.processedEventRetentionMs < 60_000) { - errors.push( - `PROCESSED_EVENT_RETENTION_MS must be >= 60000 ms ` + - `(received: ${config.cleanup.processedEventRetentionMs}).`, - ); - } - if (config.cleanup.retentionDays < 1) { - errors.push( - `CLEANUP_RETENTION_DAYS must be >= 1 (received: ${config.cleanup.retentionDays}).`, - ); - } - } - - // ── Backfill ─────────────────────────────────────────────────────────────── - if (config.backfill) { - if (config.backfill.maxLedgers < 0) { - errors.push( - `BACKFILL_MAX_LEDGERS must be >= 0 (0 = unlimited). ` + - `(received: ${config.backfill.maxLedgers}).`, - ); - } - } - - // ── RPC Rate Limiting ─────────────────────────────────────────────────────── - if (config.rpcRateLimit) { - if (config.rpcRateLimit.maxRequestsPerSecond < 1) { - errors.push( - `RPC_RATE_LIMIT_MAX_REQUESTS_PER_SECOND must be >= 1 ` + - `(received: ${config.rpcRateLimit.maxRequestsPerSecond}).`, - ); - } - if (config.rpcRateLimit.burstSize < 1) { - errors.push( - `RPC_RATE_LIMIT_BURST_SIZE must be >= 1 ` + - `(received: ${config.rpcRateLimit.burstSize}).`, - ); - } - if (config.rpcRateLimit.throttleDelayMs < 0) { - errors.push( - `RPC_RATE_LIMIT_THROTTLE_DELAY_MS must be >= 0 ` + - `(received: ${config.rpcRateLimit.throttleDelayMs}).`, - ); - } - } - - // ── Logging ──────────────────────────────────────────────────────────────── - // Rejected rather than silently downgraded: a typo in LOG_LEVEL that quietly - // resolves to "info" hides debug output an operator explicitly asked for, and - // they have no signal that the setting did not take. - if (config.logging) { - if (parseLogLevel(config.logging.level) === null) { - errors.push( - `LOG_LEVEL must be one of: ${SUPPORTED_LOG_LEVELS.join(', ')} ` + - `(received: "${config.logging.level}").`, - ); - } - - if (parseLogFormat(config.logging.format) === null) { - errors.push( - `LOG_FORMAT must be one of: ${SUPPORTED_LOG_FORMATS.join(', ')} ` + - `(received: "${config.logging.format}").`, - ); - } - } - - // ── API surface ──────────────────────────────────────────────────────────── - if (config.api) { - if (!Number.isInteger(config.api.maxBodyBytes) || config.api.maxBodyBytes <= 0) { - errors.push( - `API_MAX_BODY_BYTES must be a positive integer ` + - `(received: ${config.api.maxBodyBytes}).`, - ); - } - } - - // ── High-volume event batching ───────────────────────────────────────────── - if (config.eventBatching) { - if (config.eventBatching.maxBatchSize < 1) { - errors.push( - `EVENT_BATCHING_MAX_SIZE must be >= 1 (received: ${config.eventBatching.maxBatchSize}).`, - ); - } - if (config.eventBatching.windowMs < 0) { - errors.push( - `EVENT_BATCHING_WINDOW_MS must be >= 0 (received: ${config.eventBatching.windowMs}).`, - ); - } - } - - // ── Periodic digests ─────────────────────────────────────────────────────── - if (config.digest) { - if (config.digest.intervalMs < 1000) { - errors.push( - `DIGEST_INTERVAL_MS must be >= 1000 ms (received: ${config.digest.intervalMs}).`, - ); - } - if (config.digest.maxEventsPerDigest < 1) { - errors.push( - `DIGEST_MAX_EVENTS must be >= 1 (received: ${config.digest.maxEventsPerDigest}).`, - ); - } - } - - // ── Localization ─────────────────────────────────────────────────────────── - // Rejected rather than silently falling back: a typo in DEFAULT_LOCALE would - // otherwise send every notification in English with no signal to the operator. - if (config.localization) { - if (!SUPPORTED_LOCALES.includes(config.localization.defaultLocale)) { - errors.push( - `DEFAULT_LOCALE must be one of: ${SUPPORTED_LOCALES.join(', ')} ` + - `(received: "${config.localization.defaultLocale}").`, - ); - } - } - - if (errors.length > 0) { - throw new ConfigError( - `Configuration validation failed with ${errors.length} error(s):\n` + - errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n'), - ); - } - - // Schema-based validation check (#694) - const schemaErrors = ConfigurationSchemaValidator.validate(config as any, APP_CONFIG_SCHEMA); - if (schemaErrors.length > 0) { - throw new ConfigError( - `Configuration schema validation failed with ${schemaErrors.length} error(s):\n` + - schemaErrors.map((e, i) => ` ${i + 1}. [${e.field}] ${e.message}`).join('\n'), - ); - } - // ── Secret validation (#692) ─────────────────────────────────────────────── - // Run after structural checks so operators see both structural and secret - // problems in a single pass. Errors are reported by field name only; the - // actual secret values are never included in any message. - validateSecrets([ - { - fieldName: 'DISCORD_WEBHOOK_URL', - value: config.discord?.webhookUrl, - required: false, - }, - { - fieldName: 'DISCORD_WEBHOOK_ID', - value: config.discord?.webhookId, - required: false, - }, - // Webhook signing secrets - ...(config.webhookSecrets ?? []).map((ws, i) => ({ - fieldName: `WEBHOOK_SECRETS[${i}].secret`, - value: ws.secret, - required: true, - })), - // API keys - ...(config.apiKeys ?? []).map((ak, i) => ({ - fieldName: `API_KEYS[${i}].key`, - value: ak.key, - required: true, - })), - ]); -} + const rawValue = trimEnv(name); \ No newline at end of file diff --git a/listener/src/database/schema.sql b/listener/src/database/schema.sql index e6e969d3..12913812 100644 --- a/listener/src/database/schema.sql +++ b/listener/src/database/schema.sql @@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS notification_execution_log ( scheduled_notification_id INTEGER NOT NULL, execution_attempt INTEGER NOT NULL CHECK (execution_attempt > 0), execution_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY')), + status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY', 'DEAD_LETTERED')), error_message TEXT, response_data TEXT, -- JSON response from notification service duration_ms INTEGER CHECK (duration_ms IS NULL OR duration_ms >= 0), diff --git a/listener/src/database/template-schema.sql b/listener/src/database/template-schema.sql index 6af66782..e4fd1af7 100644 --- a/listener/src/database/template-schema.sql +++ b/listener/src/database/template-schema.sql @@ -1,8 +1,10 @@ -- Notification Templates Database Schema -- SQLite schema for storing reusable notification templates with variable placeholders --- Main table for notification templates -CREATE TABLE IF NOT EXISTS notification_templates ( +-- Main table for channel notification templates. +-- NOTE: named `channel_templates` rather than `notification_templates` so it +-- coexists with the audited template table defined in schema.sql (TEXT id). +CREATE TABLE IF NOT EXISTS channel_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- Template identification @@ -34,17 +36,17 @@ CREATE TABLE IF NOT EXISTS notification_templates ( -- Indexes for performance CREATE INDEX IF NOT EXISTS idx_templates_unique_key - ON notification_templates(unique_key); + ON channel_templates(unique_key); CREATE INDEX IF NOT EXISTS idx_templates_channel_type - ON notification_templates(channel_type); + ON channel_templates(channel_type); CREATE INDEX IF NOT EXISTS idx_templates_active - ON notification_templates(is_active) + ON channel_templates(is_active) WHERE is_active = 1; CREATE INDEX IF NOT EXISTS idx_templates_created_at - ON notification_templates(created_at DESC); + ON channel_templates(created_at DESC); -- Template usage history (for analytics) CREATE TABLE IF NOT EXISTS template_usage_log ( @@ -56,7 +58,7 @@ CREATE TABLE IF NOT EXISTS template_usage_log ( status VARCHAR(50), -- 'SUCCESS', 'FAILED' error_message TEXT, -- Error details if failed - FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE + FOREIGN KEY (template_id) REFERENCES channel_templates(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_usage_log_template_id @@ -67,10 +69,10 @@ CREATE INDEX IF NOT EXISTS idx_usage_log_rendered_at -- Trigger to update updated_at timestamp CREATE TRIGGER IF NOT EXISTS update_template_timestamp -AFTER UPDATE ON notification_templates +AFTER UPDATE ON channel_templates FOR EACH ROW BEGIN - UPDATE notification_templates + UPDATE channel_templates SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END; diff --git a/listener/src/examples/schedule-notification-example.ts b/listener/src/examples/schedule-notification-example.ts index b08f2999..a043d094 100644 --- a/listener/src/examples/schedule-notification-example.ts +++ b/listener/src/examples/schedule-notification-example.ts @@ -117,7 +117,7 @@ async function examples() { // Example 6: Get scheduler statistics // ==================================================================== const stats = await api.getStatistics(); - logger.info('Scheduler statistics', stats); + logger.info('Scheduler statistics', { ...stats }); // ==================================================================== // Example 7: Schedule notification based on blockchain event diff --git a/listener/src/index.ts b/listener/src/index.ts index 62debbca..d03405e0 100644 --- a/listener/src/index.ts +++ b/listener/src/index.ts @@ -32,7 +32,6 @@ import { loadConfig, validateConfig, ConfigError } from './config'; import { SecretValidationError } from './config/validate-secrets'; import { NotificationHealthMonitor } from './services/notification-health-monitor'; import { getWorkerManager } from './services/worker-manager'; -import { EventDeduplicationService } from './services/event-deduplication-service'; import { DeliveryReceiptRepository } from './services/delivery-receipt-repository'; import { ApiCredentialService } from './services/api-credential-service'; diff --git a/listener/src/middleware/content-type.ts b/listener/src/middleware/content-type.ts index 15e07cfe..34645e0b 100644 --- a/listener/src/middleware/content-type.ts +++ b/listener/src/middleware/content-type.ts @@ -40,7 +40,6 @@ export function sendUnsupportedMediaType( message, }); - res.setHeader('Content-Type', 'application/json'); res.writeHead(415, { 'Content-Type': 'application/json' }); res.end(payload); } @@ -61,11 +60,11 @@ export function validateContentType( } // If request explicitly specifies empty body (Content-Length: 0), allow it - if (req.headers['content-length'] === '0') { + if (req.headers?.['content-length'] === '0') { return true; } - const rawHeader = req.headers['content-type']; + const rawHeader = req.headers?.['content-type']; if (!rawHeader || (typeof rawHeader === 'string' && rawHeader.trim() === '')) { // Content-Type is omitted: allow for backward compatibility with existing clients return true; diff --git a/listener/src/middleware/security-headers.ts b/listener/src/middleware/security-headers.ts index d1880c17..fac48479 100644 --- a/listener/src/middleware/security-headers.ts +++ b/listener/src/middleware/security-headers.ts @@ -11,48 +11,12 @@ */ import type { ServerResponse } from 'http'; -import type http from 'http'; -import type { ServerResponse } from 'http'; export function addSecurityHeaders( res: ServerResponse, options: { isProduction?: boolean } = {}, - -const isLocalhost = (hostname: string): boolean => - hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'; - -export function addSecurityHeaders( - res: ServerResponse, - options: { productionOrigin?: string } = {}, ): void { res.setHeader('X-Content-Type-Options', 'nosniff'); - - // ✅ X-Frame-Options: prevent clickjacking. - // SAMEORIGIN allows embedding within same-origin iframes (dashboard use). - // DENY would break legitimate self-embedding; SAMEORIGIN is safer. - const frameOption = options.productionOrigin ? 'SAMEORIGIN' : 'SAMEORIGIN'; - res.setHeader('X-Frame-Options', frameOption); - - // ✅ X-XSS-Protection: legacy IE protection (defense-in-depth). - // No known negative impact on modern API clients or SSE. - res.setHeader('X-XSS-Protection', '1; mode=block'); - - // ✅ Referrer-Policy: control referrer information sent with requests. - // strict-origin-when-cross-origin balances privacy and functionality. - res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // ✅ Cache-Control: prevent sensitive data caching in localStorage/indexedDB - // for non-GET routes; for GET routes we allow caching where appropriate. - const method = res.statusCode >= 200 && res.statusCode < 300 ? 'public, max-age=300' : 'no-store'; - // Only set Cache-Control if not already set by a more specific handler - if (!res.getHeader('Cache-Control')) { - res.setHeader('Cache-Control', method); - } - - // ⚠️ Strict-Transport-Security: only in production with a valid origin. - // Skipped for localhost/development to avoid breaking HTTP local testing. - if (options.productionOrigin && !isLocalhost(origin || '')) { - res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); diff --git a/listener/src/migrations/004-database-data-integrity.ts b/listener/src/migrations/004-database-data-integrity.ts index b4f59fd1..74f406a6 100644 --- a/listener/src/migrations/004-database-data-integrity.ts +++ b/listener/src/migrations/004-database-data-integrity.ts @@ -36,7 +36,7 @@ const tables = [ scheduled_notification_id INTEGER NOT NULL, execution_attempt INTEGER NOT NULL CHECK (execution_attempt > 0), execution_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY')), + status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY', 'DEAD_LETTERED')), error_message TEXT, response_data TEXT, duration_ms INTEGER CHECK (duration_ms IS NULL OR duration_ms >= 0), @@ -190,7 +190,7 @@ const audits: Array<{ table: string; sql: string }> = [ table: 'notification_execution_log', sql: `SELECT COUNT(*) AS count FROM notification_execution_log l WHERE l.scheduled_notification_id IS NULL OR l.execution_attempt IS NULL OR l.execution_attempt <= 0 - OR l.execution_time IS NULL OR l.status IS NULL OR l.status NOT IN ('SUCCESS', 'FAILED', 'RETRY') + OR l.execution_time IS NULL OR l.status IS NULL OR l.status NOT IN ('SUCCESS', 'FAILED', 'RETRY', 'DEAD_LETTERED') OR (l.duration_ms IS NOT NULL AND l.duration_ms < 0) OR NOT EXISTS (SELECT 1 FROM scheduled_notifications n WHERE n.id = l.scheduled_notification_id)`, }, diff --git a/listener/src/migrations/005-notification-expiration.ts b/listener/src/migrations/005-notification-expiration.ts index a76f7ccf..3d79f389 100644 --- a/listener/src/migrations/005-notification-expiration.ts +++ b/listener/src/migrations/005-notification-expiration.ts @@ -44,7 +44,7 @@ async function auditLegacyRows(db: sqlite3.Database): Promise { table: 'notification_execution_log', sql: `SELECT COUNT(*) AS count FROM notification_execution_log l WHERE l.scheduled_notification_id IS NULL OR l.execution_attempt <= 0 - OR l.execution_time IS NULL OR l.status NOT IN ('SUCCESS', 'FAILED', 'RETRY') + OR l.execution_time IS NULL OR l.status NOT IN ('SUCCESS', 'FAILED', 'RETRY', 'DEAD_LETTERED') OR (l.duration_ms IS NOT NULL AND l.duration_ms < 0) OR NOT EXISTS (SELECT 1 FROM scheduled_notifications n WHERE n.id = l.scheduled_notification_id)`, }, @@ -168,7 +168,7 @@ const migration = { scheduled_notification_id INTEGER NOT NULL, execution_attempt INTEGER NOT NULL CHECK (execution_attempt > 0), execution_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY')), + status VARCHAR(20) NOT NULL CHECK (status IN ('SUCCESS', 'FAILED', 'RETRY', 'DEAD_LETTERED')), error_message TEXT, response_data TEXT, duration_ms INTEGER CHECK (duration_ms IS NULL OR duration_ms >= 0), diff --git a/listener/src/schema/compatibility-check.ts b/listener/src/schema/compatibility-check.ts index 47c89f2d..3f9df1eb 100644 --- a/listener/src/schema/compatibility-check.ts +++ b/listener/src/schema/compatibility-check.ts @@ -172,14 +172,6 @@ export function checkSchemaCompatibility( breakingChanges.push('Consumer event schema is empty or malformed'); } - // Check 4: Topic structure changes - // Topics are appended as trailing topics - existing consumers ignore them - // Breaking change only if the core topic (event name) changes position - if (consumer.expectedTopics && contractEvent.topics.length < consumer.expectedTopics.length) { - const missingTopics = consumer.expectedTopics.filter( - (t) => !contractEvent.topics.includes(t) - ); - if (missingTopics.length > 0) { for (const consumerEvent of consumerSchema.events) { const contractEvent = contractEvents.get(consumerEvent.eventName); if (!contractEvent) { @@ -193,20 +185,6 @@ export function checkSchemaCompatibility( safeAdditions.push(...result.safeAdditions); } - // Check 5: New data fields are safe (backward compatible) - // Any new data fields in the contract that weren't expected by the consumer - // are simply ignored - this is the Soroban trailing-topic pattern - const newDataFields = contractEvent.dataFields.filter( - (f) => !consumer.expectedFields.includes(f.name) - ); - safeAdditions.push( - ...newDataFields.map( - (f) => `Safe addition: New data field '${f.name}' in '${contractEvent.name}' (ignored by existing consumers)` - ) - ); - - // Determine compatibility - const compatible = breakingChanges.length === 0; for (const eventName of contractEvents.keys()) { if (!consumerNames.has(eventName)) { safeAdditions.push(`Safe addition: New event '${eventName}' is ignored by existing consumers`); diff --git a/listener/src/scripts/migrate-templates.ts b/listener/src/scripts/migrate-templates.ts index 5fe9aaca..75404137 100644 --- a/listener/src/scripts/migrate-templates.ts +++ b/listener/src/scripts/migrate-templates.ts @@ -44,17 +44,10 @@ async function migrateTemplates() { const schema = fs.readFileSync(schemaPath, 'utf-8'); - // Split and execute each statement - const statements = schema - .split(';') - .map((s) => s.trim()) - .filter((s) => s.length > 0); - - for (const statement of statements) { - await db.run(statement); - } + // exec runs the whole script so the CREATE TRIGGER ... BEGIN/END block works + await db.exec(schema); - logger.info('Template schema migration completed', { statements: statements.length }); + logger.info('Template schema migration completed'); // Verify tables exist const tables = await db.all<{ name: string }>( @@ -82,7 +75,7 @@ async function createSampleTemplates(db: Database) { try { // Check if templates already exist const existing = await db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM notification_templates' + 'SELECT COUNT(*) as count FROM channel_templates' ); if ((existing?.count || 0) > 0) { @@ -142,7 +135,7 @@ Status: ✅ Approved`, for (const sample of samples) { await db.run( - `INSERT INTO notification_templates ( + `INSERT INTO channel_templates ( unique_key, name, description, channel_type, subject_template, body_template, variables, default_values, is_active, created_by diff --git a/listener/src/services/discord-notification.ts b/listener/src/services/discord-notification.ts index 40704e23..438e77e6 100644 --- a/listener/src/services/discord-notification.ts +++ b/listener/src/services/discord-notification.ts @@ -424,7 +424,6 @@ export class DiscordNotificationService implements NotificationProvider { const originalLength = embed.title?.length ?? title.length; title = title.slice(0, MAX_DISCORD_EMBED_TITLE_LENGTH - 3) + '...'; logger.warn('Discord embed title truncated', { - originalLength: title.length + 3, originalLength, maxLength: MAX_DISCORD_EMBED_TITLE_LENGTH, }); @@ -449,7 +448,6 @@ export class DiscordNotificationService implements NotificationProvider { footer = { text: footer.text.slice(0, MAX_DISCORD_FOOTER_TEXT_LENGTH - 3) + '...' }; logger.warn('Discord footer text truncated', { originalLength, - originalLength: footer.text.length, maxLength: MAX_DISCORD_FOOTER_TEXT_LENGTH, }); } diff --git a/listener/src/services/event-normalization.ts b/listener/src/services/event-normalization.ts index d234e500..b939078f 100644 --- a/listener/src/services/event-normalization.ts +++ b/listener/src/services/event-normalization.ts @@ -80,7 +80,7 @@ export function normalizeSorobanEvent( return { ok: false, errors }; } - const validation = validateEventPayload(event as StellarSDK.rpc.Api.EventResponse); + const validation = validateEventPayload(event as unknown as StellarSDK.rpc.Api.EventResponse); if (!validation.valid && validation.error) { errors.push(validation.error); } @@ -89,7 +89,7 @@ export function normalizeSorobanEvent( return { ok: false, errors }; } - const raw = event as StellarSDK.rpc.Api.EventResponse; + const raw = event as unknown as StellarSDK.rpc.Api.EventResponse; let topic: string[]; let value: string; let eventName: string | null; diff --git a/listener/src/services/event-processing-queue.test.ts b/listener/src/services/event-processing-queue.test.ts index 811f55de..b7e72ae7 100644 --- a/listener/src/services/event-processing-queue.test.ts +++ b/listener/src/services/event-processing-queue.test.ts @@ -8,6 +8,7 @@ import { jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/services/event-subscriber-backfill.test.ts b/listener/src/services/event-subscriber-backfill.test.ts index 334290b6..6c3b6c32 100644 --- a/listener/src/services/event-subscriber-backfill.test.ts +++ b/listener/src/services/event-subscriber-backfill.test.ts @@ -30,7 +30,7 @@ jest.mock('@stellar/stellar-sdk', () => { jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); jest.mock('./discord-notification', () => ({ diff --git a/listener/src/services/historical-backfill-service.ts b/listener/src/services/historical-backfill-service.ts index b28f0883..7922da8d 100644 --- a/listener/src/services/historical-backfill-service.ts +++ b/listener/src/services/historical-backfill-service.ts @@ -131,7 +131,7 @@ export class HistoricalBackfillService { }; } - const { startLedger } = this.backfillConfig; + const startLedger = this.backfillConfig.startLedger ?? 1; logger.info('Historical backfill starting', { requestId, diff --git a/listener/src/services/idempotency-key-service.test.ts b/listener/src/services/idempotency-key-service.test.ts index 49b264b7..c64952e8 100644 --- a/listener/src/services/idempotency-key-service.test.ts +++ b/listener/src/services/idempotency-key-service.test.ts @@ -9,7 +9,7 @@ import logger from '../utils/logger'; jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); describe('IdempotencyKeyService', () => { diff --git a/listener/src/services/notification-deduplicator.test.ts b/listener/src/services/notification-deduplicator.test.ts index 31a1fdd4..b99eac71 100644 --- a/listener/src/services/notification-deduplicator.test.ts +++ b/listener/src/services/notification-deduplicator.test.ts @@ -3,7 +3,7 @@ import logger from '../utils/logger'; jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); const mockLogger = logger as jest.Mocked; diff --git a/listener/src/services/notification-deduplicator.ts b/listener/src/services/notification-deduplicator.ts index 44e555e6..ce008862 100644 --- a/listener/src/services/notification-deduplicator.ts +++ b/listener/src/services/notification-deduplicator.ts @@ -183,6 +183,15 @@ export class NotificationDeduplicator { this.events.length = 0; } + /** + * Reset the deduplicator to its initial state: empty cache, metrics, and events. + */ + clear(): void { + this.seen.clear(); + this.events.length = 0; + this.resetMetrics(); + } + /** * Reset all metrics (useful for testing or metrics reporting). */ diff --git a/listener/src/services/notification-expiration.ts b/listener/src/services/notification-expiration.ts index 14776bc4..ca30edbf 100644 --- a/listener/src/services/notification-expiration.ts +++ b/listener/src/services/notification-expiration.ts @@ -2,6 +2,15 @@ import * as StellarSDK from '@stellar/stellar-sdk'; import { ExpirationConfig } from '../types'; import logger from '../utils/logger'; +/** + * Event shape consumed for expiration checks. + * + * The Stellar RPC `EventResponse` carries no arrival timestamp, so + * `receivedAt` (ms since epoch) is optional here and events without it are + * treated as having just arrived. + */ +type ExpirableEvent = StellarSDK.rpc.Api.EventResponse & { receivedAt?: number }; + /** * NotificationExpirationService handles expiration checks for notifications. * @@ -25,7 +34,7 @@ export class NotificationExpirationService { * @param event - The blockchain event to check * @returns true if the event has expired, false otherwise */ - isExpired(event: StellarSDK.rpc.Api.EventResponse, eventType?: string): boolean { + isExpired(event: ExpirableEvent, eventType?: string): boolean { // If expiration is disabled, nothing is ever expired if (!this.config.enabled) { return false; @@ -36,7 +45,8 @@ export class NotificationExpirationService { // Calculate when this event should expire // receivedAt is in milliseconds (Unix timestamp) - const expiresAtMs = event.receivedAt + expirationTimeMs; + const receivedAt = event.receivedAt ?? Date.now(); + const expiresAtMs = receivedAt + expirationTimeMs; // Check if current time exceeds the expiration time const currentTimeMs = Date.now(); @@ -53,7 +63,7 @@ export class NotificationExpirationService { * @returns true if the event should be processed, false if expired */ shouldProcess( - event: StellarSDK.rpc.Api.EventResponse, + event: ExpirableEvent, eventType?: string ): boolean { // If expiration is disabled, always process diff --git a/listener/src/services/notification-retry-queue-refactored.test.ts b/listener/src/services/notification-retry-queue-refactored.test.ts index f2670519..a7248900 100644 --- a/listener/src/services/notification-retry-queue-refactored.test.ts +++ b/listener/src/services/notification-retry-queue-refactored.test.ts @@ -11,6 +11,7 @@ import { NotificationFixtureBuilder } from '../test-utils/notification-fixture-b jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/services/notification-retry-queue.test.ts b/listener/src/services/notification-retry-queue.test.ts index 59c610af..e570170e 100644 --- a/listener/src/services/notification-retry-queue.test.ts +++ b/listener/src/services/notification-retry-queue.test.ts @@ -9,6 +9,7 @@ import { jest.mock('../utils/logger', () => ({ __esModule: true, default: { + debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), diff --git a/listener/src/services/notification-retry-queue.ts b/listener/src/services/notification-retry-queue.ts index fa1fe57c..dc75a7a2 100644 --- a/listener/src/services/notification-retry-queue.ts +++ b/listener/src/services/notification-retry-queue.ts @@ -6,6 +6,12 @@ import { getEventName } from '../utils/event-utils'; import { getNotificationAnalyticsAggregator, NotificationAnalyticsAggregator } from './notification-analytics-aggregator'; import { NotificationType } from '../types/scheduled-notification'; import { RetryFailureType, RetryPolicy, RetryPolicyConfig, classifyError } from './retry-policy'; +import { + PartialRetryBackoffConfig, + RetryBackoffConfig, + resolveRetryBackoffConfig, + calculateBackoffDelay, +} from '../utils/retry-backoff-config'; export enum Priority { Low = 0, @@ -80,9 +86,9 @@ export class NotificationRetryQueue { constructor(notificationFn: NotificationFn, options?: RetryQueueOptions) { this.notificationFn = notificationFn; - this.backoff = resolveRetryBackoffConfig(options.backoff); - this.processIntervalMs = options.processIntervalMs ?? DEFAULTS.processIntervalMs; - this.priorityWeights = options.priorityWeights ?? DEFAULTS.priorityWeights; + this.backoff = resolveRetryBackoffConfig(options?.backoff); + this.processIntervalMs = options?.processIntervalMs ?? DEFAULTS.processIntervalMs; + this.priorityWeights = options?.priorityWeights ?? DEFAULTS.priorityWeights; this.analytics = getNotificationAnalyticsAggregator(); this.policy = new RetryPolicy(options?.retryPolicy); } @@ -236,7 +242,7 @@ export class NotificationRetryQueue { } const failureType = classifyError(outcome.error); - const decision = this.policy.evaluate(failureType, attempt, this.maxRetries); + const decision = this.policy.evaluate(failureType, attempt, this.backoff.maxRetries); if (!decision.shouldRetry) { this.queuedFingerprints.delete(fingerprint); diff --git a/listener/src/services/payload-validation.integration.test.ts b/listener/src/services/payload-validation.integration.test.ts index e459b698..3c2e80c5 100644 --- a/listener/src/services/payload-validation.integration.test.ts +++ b/listener/src/services/payload-validation.integration.test.ts @@ -15,7 +15,7 @@ import { DEFAULT_MAX_PAYLOAD_SIZE_BYTES } from '../utils/payload-size-validator' jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); jest.mock('../store/event-registry', () => ({ diff --git a/listener/src/services/providers/discord-provider.ts b/listener/src/services/providers/discord-provider.ts index 84c02c93..9d5735e4 100644 --- a/listener/src/services/providers/discord-provider.ts +++ b/listener/src/services/providers/discord-provider.ts @@ -64,6 +64,7 @@ export class DiscordNotificationProvider implements NotificationProvider { const providerResponse = { statusCode: response.status }; if (!response.ok) { + const errorText = await response.text().catch(() => ''); logger.warn('Discord provider: webhook responded with non-OK status', { requestId, targetRecipient: sanitizeCredentials(targetRecipient), @@ -74,6 +75,10 @@ export class DiscordNotificationProvider implements NotificationProvider { success: false, degradedCapabilities, errorMessage: `HTTP ${response.status}: ${sanitizeCredentials(errorText)}`, + errorCode: `HTTP_${response.status}`, + statusCode: response.status, + providerMessageId, + providerResponse, }; } @@ -81,7 +86,7 @@ export class DiscordNotificationProvider implements NotificationProvider { requestId, targetRecipient: sanitizeCredentials(targetRecipient), }); - return { success: true, degradedCapabilities }; + return { success: true, degradedCapabilities, providerMessageId, providerResponse }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const sanitized = sanitizeCredentials(errorMessage); diff --git a/listener/src/services/retry-scheduler.ts b/listener/src/services/retry-scheduler.ts index ff6c67c3..e101bd2e 100644 --- a/listener/src/services/retry-scheduler.ts +++ b/listener/src/services/retry-scheduler.ts @@ -17,6 +17,8 @@ import { classifyHttpStatus, } from './retry-policy'; import { startClaimLease } from './notification-claim-lease'; +import { RETRY_BACKOFF_DEFAULTS } from '../utils/retry-backoff-config'; +import type { RetryBackoffConfig } from '../utils/retry-backoff-config'; export interface RetrySchedulerConfig { /** Whether the scheduler is enabled. */ @@ -73,7 +75,8 @@ export const RETRY_SCHEDULER_DEFAULTS: Readonly { const sql = ` - INSERT INTO notification_templates ( + INSERT INTO channel_templates ( unique_key, name, description, channel_type, subject_template, body_template, variables, default_values, is_active, created_by @@ -59,7 +59,7 @@ export class TemplateRepository { * Get template by ID */ async getById(id: number): Promise { - const sql = 'SELECT * FROM notification_templates WHERE id = ?'; + const sql = 'SELECT * FROM channel_templates WHERE id = ?'; const row = await this.db.get(sql, [id]); return row ? this.rowToModel(row) : null; @@ -69,7 +69,7 @@ export class TemplateRepository { * Get template by unique key */ async getByUniqueKey(uniqueKey: string): Promise { - const sql = 'SELECT * FROM notification_templates WHERE unique_key = ?'; + const sql = 'SELECT * FROM channel_templates WHERE unique_key = ?'; const row = await this.db.get(sql, [uniqueKey]); return row ? this.rowToModel(row) : null; @@ -84,7 +84,7 @@ export class TemplateRepository { limit?: number; offset?: number; }): Promise { - let sql = 'SELECT * FROM notification_templates WHERE 1=1'; + let sql = 'SELECT * FROM channel_templates WHERE 1=1'; const params: any[] = []; if (filters?.channelType) { @@ -169,7 +169,7 @@ export class TemplateRepository { params.push(id); const sql = ` - UPDATE notification_templates + UPDATE channel_templates SET ${updates.join(', ')} WHERE id = ? `; @@ -188,7 +188,7 @@ export class TemplateRepository { * Delete template (soft delete by marking inactive) */ async deactivate(id: number): Promise { - const sql = 'UPDATE notification_templates SET is_active = 0 WHERE id = ?'; + const sql = 'UPDATE channel_templates SET is_active = 0 WHERE id = ?'; const result = await this.db.run(sql, [id]); if (result.changes > 0) { @@ -203,7 +203,7 @@ export class TemplateRepository { * Hard delete template (permanent deletion) */ async delete(id: number): Promise { - const sql = 'DELETE FROM notification_templates WHERE id = ?'; + const sql = 'DELETE FROM channel_templates WHERE id = ?'; const result = await this.db.run(sql, [id]); if (result.changes > 0) { @@ -218,7 +218,7 @@ export class TemplateRepository { * Check if unique key exists */ async exists(uniqueKey: string): Promise { - const sql = 'SELECT COUNT(*) as count FROM notification_templates WHERE unique_key = ?'; + const sql = 'SELECT COUNT(*) as count FROM channel_templates WHERE unique_key = ?'; const result = await this.db.get<{ count: number }>(sql, [uniqueKey]); return (result?.count || 0) > 0; } @@ -282,7 +282,7 @@ export class TemplateRepository { async getCountByChannel(): Promise> { const sql = ` SELECT channel_type, COUNT(*) as count - FROM notification_templates + FROM channel_templates WHERE is_active = 1 GROUP BY channel_type `; diff --git a/listener/src/services/webhook-delivery-service.test.ts b/listener/src/services/webhook-delivery-service.test.ts index 60019169..d11b61d9 100644 --- a/listener/src/services/webhook-delivery-service.test.ts +++ b/listener/src/services/webhook-delivery-service.test.ts @@ -43,7 +43,12 @@ const mockSendWebhook = sendWebhook as jest.MockedFunction; // --------------------------------------------------------------------------- function makeResponse(status: number, ok = status >= 200 && status < 300): Response { - return { status, ok, text: jest.fn<() => Promise>().mockResolvedValue('') } as unknown as Response; + return { + status, + ok, + headers: { get: () => null }, + text: jest.fn<() => Promise>().mockResolvedValue(''), + } as unknown as Response; } function makeAbortError(): Error { diff --git a/listener/src/services/webhook-delivery-service.ts b/listener/src/services/webhook-delivery-service.ts index 56babe6d..9ea239f3 100644 --- a/listener/src/services/webhook-delivery-service.ts +++ b/listener/src/services/webhook-delivery-service.ts @@ -17,12 +17,8 @@ */ import logger from '../utils/logger'; -import { - sendWebhook, - WebhookSendOptions, - WebhookFailureReason, - isWebhookTimeoutError, -} from './webhook-sender'; +import { sendWebhook, WebhookSendOptions, WebhookFailureReason } from './webhook-sender'; +import { classifyError, RetryFailureType } from './retry-policy'; /** * Default timeout applied to an outbound webhook request when the operator has @@ -52,7 +48,7 @@ export interface WebhookDeliveryResult { statusCode?: number; /** Human-readable failure reason for logging. */ errorReason?: string; -providerMessageId?: string; + providerMessageId?: string; providerResponse?: Record; errorCode?: string; /** @@ -124,7 +120,7 @@ export class WebhookDeliveryService { success: false, statusCode: response.status, errorReason: `HTTP ${response.status}`, -errorCode: `HTTP_${response.status}`, + errorCode: `HTTP_${response.status}`, providerMessageId, providerResponse, failureReason: 'http_retryable', @@ -141,16 +137,15 @@ errorCode: `HTTP_${response.status}`, success: false, statusCode: response.status, errorReason: `HTTP ${response.status}`, -failureReason: 'http_permanent', + failureReason: 'http_permanent', errorCode: `HTTP_${response.status}`, providerMessageId, providerResponse, }; } catch (err) { const durationMs = Date.now() - startMs; - const isTimeout = err instanceof Error && err.name === 'AbortError'; const isDnsBlocked = err instanceof Error && err.name === 'DnsRebindingError'; - const isTimeout = isWebhookTimeoutError(err); + const isTimeout = classifyError(err) === RetryFailureType.Timeout; const errorReason = isTimeout ? `Webhook request timed out after ${timeoutMs}ms` : err instanceof Error @@ -180,7 +175,7 @@ failureReason: 'http_permanent', }); } -return { + return { success: false, errorReason, failureReason: isTimeout ? 'timeout' : 'network', diff --git a/listener/src/services/webhook-retry-helper.ts b/listener/src/services/webhook-retry-helper.ts index 7b838477..86ae34da 100644 --- a/listener/src/services/webhook-retry-helper.ts +++ b/listener/src/services/webhook-retry-helper.ts @@ -16,8 +16,8 @@ * returned to the caller immediately. */ -import { sendWebhook, WebhookSendOptions, WebhookFailureReason, isWebhookTimeoutError } from './webhook-sender'; -import { RetryFailureType, RetryPolicy, classifyError, classifyHttpStatus } from './retry-policy'; +import { sendWebhook, WebhookSendOptions, WebhookFailureReason } from './webhook-sender'; +import { RetryFailureType, RetryPolicy, classifyError } from './retry-policy'; /** Maximum number of retry attempts (not counting the initial attempt). */ const MAX_RETRY_ATTEMPTS = 2; @@ -25,6 +25,12 @@ const MAX_RETRY_ATTEMPTS = 2; /** Delay in milliseconds before the first retry. */ const RETRY_DELAY_MS = 1000; +/** Client errors that will never succeed on retry. */ +const PERMANENT_CLIENT_ERRORS = new Set([400, 401, 403, 404, 410, 422]); + +/** Statuses explicitly worth retrying; any other 5xx is also retryable. */ +const RETRYABLE_STATUS_CODES = new Set([408, 429]); + export interface WebhookRetryOptions extends WebhookSendOptions { /** * Retry policy overrides. Defaults to @@ -62,7 +68,7 @@ export function classifyWebhookFailure( // A thrown error means no HTTP response was available. Timeouts are their // own reason; everything else is a network-level failure. if (error) { - return isWebhookTimeoutError(error) ? 'timeout' : 'network'; + return classifyError(error) === RetryFailureType.Timeout ? 'timeout' : 'network'; } if (!response || response.ok) { @@ -139,7 +145,7 @@ export async function sendWebhookWithRetry( } // Non-retryable failure (permanent client error) - if (!isRetryable(response, undefined, policy)) { + if (!isRetryable(response, undefined)) { return response; } diff --git a/listener/src/services/webhook-sender.ts b/listener/src/services/webhook-sender.ts index a591bd32..4ad5245a 100644 --- a/listener/src/services/webhook-sender.ts +++ b/listener/src/services/webhook-sender.ts @@ -18,13 +18,6 @@ export interface WebhookSendOptions { } /** - * Sends a single outbound POST webhook to `url`. - * - * Before the HTTP request is made the destination is validated against the - * DNS rebinding guard (issue #994). If the resolved address falls within any - * blocked range (RFC 1918, loopback, link-local, etc.) the request is - * rejected with a `DnsRebindingError` before a single byte leaves the process. - */ * Classification of an outbound webhook failure. * * Kept deliberately coarse so that retry decisions and observability can tell @@ -53,6 +46,14 @@ export function isWebhookTimeoutError(error: unknown): boolean { ); } +/** + * Sends a single outbound POST webhook to `url`. + * + * Before the HTTP request is made the destination is validated against the + * DNS rebinding guard (issue #994). If the resolved address falls within any + * blocked range (RFC 1918, loopback, link-local, etc.) the request is + * rejected with a `DnsRebindingError` before a single byte leaves the process. + */ export async function sendWebhook( url: string, payload: any, diff --git a/listener/src/services/webhook-verifier.test.ts b/listener/src/services/webhook-verifier.test.ts index ce9192b7..fc4d7428 100644 --- a/listener/src/services/webhook-verifier.test.ts +++ b/listener/src/services/webhook-verifier.test.ts @@ -15,7 +15,7 @@ import { WebhookReplayCache } from './webhook-replay-cache'; jest.mock('../utils/logger', () => ({ __esModule: true, - default: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + default: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }, })); const mockLogger = logger as jest.Mocked; @@ -534,9 +534,6 @@ describe('verifyWebhookRequest — end-to-end request authentication', () => { // this test keeps exercising the key-swap path specifically. const ts = Math.floor(Date.now() / 1000).toString(); const sig = computeWebhookSignature(payload, SECRETS[1].secret, ts); - const outcome = verifyWebhookRequest({ - // Signed with key-beta's secret but presented as key-alpha - const sig = computeWebhookSignature(payload, SECRETS[1].secret); const outcome = await verifyWebhookRequest({ headers: { 'x-webhook-signature': sig, @@ -594,8 +591,8 @@ describe('Issue #853 — replay protection at the request boundary', () => { }; } - it('REJECTS a request that carries no timestamp (would otherwise replay forever)', () => { - const outcome = verifyWebhookRequest({ + it('REJECTS a request that carries no timestamp (would otherwise replay forever)', async () => { + const outcome = await verifyWebhookRequest({ headers: signedHeaders(BODY, null), rawBody: BODY, secrets: SECRETS, @@ -606,8 +603,8 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(outcome.errorCode).toBe('AUTH_MISSING_TIMESTAMP'); }); - it('ACCEPTS the same timestamp-less request when legacy mode is opted into', () => { - const outcome = verifyWebhookRequest({ + it('ACCEPTS the same timestamp-less request when legacy mode is opted into', async () => { + const outcome = await verifyWebhookRequest({ headers: signedHeaders(BODY, null), rawBody: BODY, secrets: SECRETS, @@ -616,13 +613,13 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(outcome.authenticated).toBe(true); }); - it('REJECTS a captured request replayed after the freshness window has passed', () => { + it('REJECTS a captured request replayed after the freshness window has passed', async () => { const realNow = Date.now; try { const ts = String(Math.floor(Date.now() / 1000)); const headers = signedHeaders(BODY, ts); - const first = verifyWebhookRequest({ + const first = await verifyWebhookRequest({ headers, rawBody: BODY, secrets: SECRETS, @@ -632,7 +629,7 @@ describe('Issue #853 — replay protection at the request boundary', () => { // Replay one hour later with the exact same captured headers. Date.now = () => realNow() + 60 * 60 * 1000; - const replay = verifyWebhookRequest({ + const replay = await verifyWebhookRequest({ headers, rawBody: BODY, secrets: SECRETS, @@ -646,12 +643,12 @@ describe('Issue #853 — replay protection at the request boundary', () => { } }); - it('REJECTS a verbatim replay inside the freshness window via the replay cache', () => { + it('REJECTS a verbatim replay inside the freshness window via the replay cache', async () => { const replayCache = new WebhookReplayCache(); const ts = String(Math.floor(Date.now() / 1000)); const headers = signedHeaders(BODY, ts); - const first = verifyWebhookRequest({ + const first = await verifyWebhookRequest({ headers, rawBody: BODY, secrets: SECRETS, @@ -661,7 +658,7 @@ describe('Issue #853 — replay protection at the request boundary', () => { // Same signature, no Idempotency-Key header — the pre-existing replay // defence the attacker would trivially bypass. - const second = verifyWebhookRequest({ + const second = await verifyWebhookRequest({ headers, rawBody: BODY, secrets: SECRETS, @@ -677,18 +674,18 @@ describe('Issue #853 — replay protection at the request boundary', () => { ); }); - it('replay cache does not reject a genuinely new request from the same key', () => { + it('replay cache does not reject a genuinely new request from the same key', async () => { const replayCache = new WebhookReplayCache(); const ts = String(Math.floor(Date.now() / 1000)); - const a = verifyWebhookRequest({ + const a = await verifyWebhookRequest({ headers: signedHeaders(BODY, ts), rawBody: BODY, secrets: SECRETS, replayCache, }); const other = '{"action":"transfer","amount":20}'; - const b = verifyWebhookRequest({ + const b = await verifyWebhookRequest({ headers: signedHeaders(other, ts), rawBody: other, secrets: SECRETS, @@ -699,11 +696,11 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(b.authenticated).toBe(true); }); - it('does not record invalid signatures in the replay cache', () => { + it('does not record invalid signatures in the replay cache', async () => { const replayCache = new WebhookReplayCache(); const ts = String(Math.floor(Date.now() / 1000)); - const bad = verifyWebhookRequest({ + const bad = await verifyWebhookRequest({ headers: signedHeaders(BODY, ts, 'some_other_secret'), rawBody: BODY, secrets: SECRETS, @@ -712,7 +709,7 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(bad.authenticated).toBe(false); // The genuine request must still be accepted afterwards. - const good = verifyWebhookRequest({ + const good = await verifyWebhookRequest({ headers: signedHeaders(BODY, ts), rawBody: BODY, secrets: SECRETS, @@ -721,9 +718,9 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(good.authenticated).toBe(true); }); - it('REJECTS a non-integer timestamp instead of silently truncating it', () => { + it('REJECTS a non-integer timestamp instead of silently truncating it', async () => { const ts = `${Math.floor(Date.now() / 1000)}garbage`; - const outcome = verifyWebhookRequest({ + const outcome = await verifyWebhookRequest({ headers: signedHeaders(BODY, ts), rawBody: BODY, secrets: SECRETS, @@ -733,9 +730,9 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); }); - it('REJECTS a fractional timestamp', () => { + it('REJECTS a fractional timestamp', async () => { const ts = `${Math.floor(Date.now() / 1000)}.9`; - const outcome = verifyWebhookRequest({ + const outcome = await verifyWebhookRequest({ headers: signedHeaders(BODY, ts), rawBody: BODY, secrets: SECRETS, @@ -744,10 +741,10 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(outcome.errorCode).toBe('AUTH_TIMESTAMP_EXPIRED'); }); - it('accepts an uppercase hex signature (hex is case-insensitive)', () => { + it('accepts an uppercase hex signature (hex is case-insensitive)', async () => { const ts = String(Math.floor(Date.now() / 1000)); const headers = signedHeaders(BODY, ts); - const outcome = verifyWebhookRequest({ + const outcome = await verifyWebhookRequest({ headers: { ...headers, 'x-webhook-signature': headers['x-webhook-signature'].toUpperCase(), @@ -759,13 +756,13 @@ describe('Issue #853 — replay protection at the request boundary', () => { expect(outcome.authenticated).toBe(true); }); - it('still rejects a genuine signature whose hex case was altered', () => { + it('still rejects a genuine signature whose hex case was altered', async () => { const ts = String(Math.floor(Date.now() / 1000)); const headers = signedHeaders(BODY, ts); const tampered = headers['x-webhook-signature'].replace(/.$/, (c) => c === 'a' ? 'b' : 'a' ); - const outcome = verifyWebhookRequest({ + const outcome = await verifyWebhookRequest({ headers: { ...headers, 'x-webhook-signature': tampered }, rawBody: BODY, secrets: SECRETS, diff --git a/listener/src/services/webhook-verifier.ts b/listener/src/services/webhook-verifier.ts index 14945918..c2a92cb8 100644 --- a/listener/src/services/webhook-verifier.ts +++ b/listener/src/services/webhook-verifier.ts @@ -101,13 +101,6 @@ export function verifySignature( const hasTimestamp = timestampHeader !== undefined && timestampHeader !== null && timestampHeader !== ''; - if (!hasTimestamp && options?.requireTimestamp) { - logger.warn('Webhook signature verification failed: timestamp required but absent', { - ...auditContext, - }); - return { valid: false, reason: 'missing_timestamp' }; - } - // Validate timestamp expiration if provided if (hasTimestamp && options?.maxAgeSeconds !== undefined) { if (!isTimestampValid(timestampHeader as string, options.maxAgeSeconds)) { @@ -147,6 +140,15 @@ export function verifySignature( return { valid: false, reason: 'hmac_mismatch' }; } + // A signature that verifies without a timestamp is only acceptable when the + // caller has explicitly opted into legacy (timestamp-less) signing. + if (!hasTimestamp && options?.requireTimestamp) { + logger.warn('Webhook signature verification failed: timestamp required but absent', { + ...auditContext, + }); + return { valid: false, reason: 'missing_timestamp' }; + } + return { valid: true }; } @@ -295,7 +297,7 @@ const AUTH_ERRORS: Record { const { headers, rawBody, @@ -306,9 +308,8 @@ export function verifyWebhookRequest(ctx: WebhookVerificationContext): WebhookVe maxAgeSeconds, requireTimestamp = true, replayCache, + auditService, } = ctx; -export async function verifyWebhookRequest(ctx: WebhookVerificationContext): Promise { - const { headers, rawBody, secrets, sourceIp, requestId, correlationId, maxAgeSeconds, auditService } = ctx; const signatureHeader = extractSignature(headers); const keyId = extractKeyId(headers); @@ -322,20 +323,40 @@ export async function verifyWebhookRequest(ctx: WebhookVerificationContext): Pro contentLength: rawBody.length, }; - if (!signatureHeader) { - logger.warn('Webhook authentication rejected: missing signature header', auditContext); - const e = AUTH_ERRORS.missing_signature_header; - if (auditService) { + /** + * Persist an authentication failure to the security audit trail. + * + * Best effort by design: a failing audit sink must never change the auth + * decision, so the error is logged instead of propagating to the caller. + */ + const recordAuditFailure = async ( + error: { code: string; message: string }, + actor: string, + ): Promise => { + if (!auditService) return; + try { await auditService.record({ action: 'auth_failure', - actor: 'unknown', + actor, sourceIp, requestId, correlationId, - outcome: e.code, - details: { reason: e.message }, + outcome: error.code, + details: { reason: error.message }, + }); + } catch (err) { + logger.error('Failed to write security audit record', { + ...auditContext, + outcome: error.code, + error: err instanceof Error ? err.message : String(err), }); } + }; + + if (!signatureHeader) { + logger.warn('Webhook authentication rejected: missing signature header', auditContext); + const e = AUTH_ERRORS.missing_signature_header; + await recordAuditFailure(e, 'unknown'); return { authenticated: false, statusCode: e.status, @@ -348,17 +369,7 @@ export async function verifyWebhookRequest(ctx: WebhookVerificationContext): Pro if (!keyId) { logger.warn('Webhook authentication rejected: missing key-id header', auditContext); const e = AUTH_ERRORS.missing_key_id; - if (auditService) { - await auditService.record({ - action: 'auth_failure', - actor: 'unknown', - sourceIp, - requestId, - correlationId, - outcome: e.code, - details: { reason: e.message }, - }); - } + await recordAuditFailure(e, 'unknown'); return { authenticated: false, statusCode: e.status, @@ -372,17 +383,7 @@ export async function verifyWebhookRequest(ctx: WebhookVerificationContext): Pro if (!secret) { logger.warn('Webhook authentication rejected: unknown key-id', { ...auditContext, keyId }); const e = AUTH_ERRORS.unknown_key_id; - if (auditService) { - await auditService.record({ - action: 'auth_failure', - actor: keyId, - sourceIp, - requestId, - correlationId, - outcome: e.code, - details: { reason: e.message }, - }); - } + await recordAuditFailure(e, keyId); return { authenticated: false, statusCode: e.status, @@ -406,17 +407,7 @@ export async function verifyWebhookRequest(ctx: WebhookVerificationContext): Pro if (!verification.valid) { const err = AUTH_ERRORS[verification.reason ?? 'hmac_mismatch'] ?? AUTH_ERRORS.hmac_mismatch; - if (auditService) { - await auditService.record({ - action: 'auth_failure', - actor: keyId, - sourceIp, - requestId, - correlationId, - outcome: err.code, - details: { reason: err.message }, - }); - } + await recordAuditFailure(err, keyId); return { authenticated: false, statusCode: err.status, diff --git a/listener/src/tests/events-server.test.ts b/listener/src/tests/events-server.test.ts index 9261b7db..77a270f9 100644 --- a/listener/src/tests/events-server.test.ts +++ b/listener/src/tests/events-server.test.ts @@ -6,12 +6,10 @@ const TEST_PORT = 19876; function makeRequest( path: string, options: { headers?: Record; method?: string; port?: number } = {} -): Promise<{ status: number; headers: http.IncomingHttpHeaders }> { - options: { headers?: Record; method?: string } = {} ): Promise<{ status: number; headers: http.IncomingHttpHeaders; body: any }> { return new Promise((resolve, reject) => { const req = http.request( - { hostname: '127.0.0.1', port: options.port ?? TEST_PORT, path, method: options.method ?? 'GET', headers: options.headers }, + { hostname: '127.0.0.1', port: options.port ?? TEST_PORT, path, method: options.method ?? 'GET', headers: options.headers, agent: false }, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); @@ -20,6 +18,7 @@ function makeRequest( try { body = JSON.parse(data); } catch (e) {} resolve({ status: res.statusCode ?? 0, headers: res.headers, body }); }); + } ); req.on('error', reject); req.end(); @@ -74,7 +73,6 @@ describe('correlation ID propagation', () => { }); describe('security headers', () => { -describe('GET /api/events pagination', () => { let server: http.Server; beforeAll((done) => { @@ -85,22 +83,6 @@ describe('GET /api/events pagination', () => { contractAddresses: [], }); server.listen(TEST_PORT, done); - - // Add 150 mock events to the registry - const { eventRegistry } = require('../store/event-registry'); - for (let i = 0; i < 150; i++) { - eventRegistry.addEvent({ - eventId: `0000000000000000-${i}`, - contractAddress: 'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', - eventName: 'autoshare_created', - ledger: 1000 + i, - type: 'contract', - topic: ['test'], - value: 'test', - txHash: '0x123', - receivedAt: Date.now() - }); - } }); afterAll((done) => { @@ -146,34 +128,70 @@ describe('GET /api/events pagination', () => { } finally { await new Promise((resolve) => productionServer.close(() => resolve())); } + }); +}); + +describe('GET /api/events pagination', () => { + let server: http.Server; + + beforeAll((done) => { + server = createEventsServer({ + port: TEST_PORT, + stellarRpcUrl: 'http://localhost:8000', + stellarNetworkPassphrase: 'Test SDF Network ; September 2015', + contractAddresses: [], + }); + server.listen(TEST_PORT, done); + + // Add 150 mock events to the registry + const { eventRegistry } = require('../store/event-registry'); + const { xdr } = require('@stellar/stellar-sdk'); + for (let i = 0; i < 150; i++) { + eventRegistry.addFromInput({ + eventId: `0000000000000000-${i}`, + contractAddress: 'CCEMX6JKPEUGYAOU4YZP3WBXGPWK7AEFDEDLRXFIDIJPQFMRXTHUVIO', + eventName: 'autoshare_created', + ledger: 1000 + i, + type: 'contract', + topic: [xdr.ScVal.scvSymbol('test')], + value: xdr.ScVal.scvString('test'), + txHash: '0x123', + }); + } + }); + + afterAll((done) => { + server.close(done); + }); + test('defaults to 20 items when limit is not provided', async () => { const { status, body } = await makeRequest('/api/events'); expect(status).toBe(200); - expect(body.events.length).toBe(20); - expect(body.count).toBeGreaterThanOrEqual(150); + expect(body.data.events.length).toBe(20); + expect(body.data.count).toBeGreaterThanOrEqual(150); }); test('respects limit when provided within bounds', async () => { const { status, body } = await makeRequest('/api/events?limit=50'); expect(status).toBe(200); - expect(body.events.length).toBe(50); + expect(body.data.events.length).toBe(50); }); test('caps excessively large limit at 100', async () => { const { status, body } = await makeRequest('/api/events?limit=200'); expect(status).toBe(200); - expect(body.events.length).toBe(100); + expect(body.data.events.length).toBe(100); }); test('handles negative limits by falling back to minimum (1)', async () => { const { status, body } = await makeRequest('/api/events?limit=-10'); expect(status).toBe(200); - expect(body.events.length).toBe(1); + expect(body.data.events.length).toBe(1); }); - + test('handles invalid limit by falling back to default', async () => { const { status, body } = await makeRequest('/api/events?limit=invalid'); expect(status).toBe(200); - expect(body.events.length).toBe(20); + expect(body.data.events.length).toBe(20); }); }); diff --git a/listener/src/tests/notification-scheduler-refactored.test.ts b/listener/src/tests/notification-scheduler-refactored.test.ts index cd03f166..7b75813a 100644 --- a/listener/src/tests/notification-scheduler-refactored.test.ts +++ b/listener/src/tests/notification-scheduler-refactored.test.ts @@ -181,8 +181,8 @@ describe('NotificationScheduler (Refactored)', () => { await repository.markAsFailedOrRetry(id, error, 3, 3); const notification = await repository.getById(id); - expect(notification!.status).toBe(NotificationStatus.FAILED); - expect(notification!.retryCount).toBe(4); + expect(notification!.status).toBe(NotificationStatus.DEAD_LETTERED); + expect(notification!.retryCount).toBe(3); }); test('should cancel pending notification', async () => { @@ -277,7 +277,7 @@ describe('NotificationScheduler (Refactored)', () => { expect(recovered).toBe(1); const notification = await repository.getById(id); - expect(notification!.status).toBe(NotificationStatus.FAILED); + expect(notification!.status).toBe(NotificationStatus.DEAD_LETTERED); expect(notification!.retryCount).toBe(1); // Verify execution log @@ -286,7 +286,7 @@ describe('NotificationScheduler (Refactored)', () => { [id], ); expect(logs.length).toBe(1); - expect(logs[0].status).toBe('FAILED'); + expect(logs[0].status).toBe('DEAD_LETTERED'); }); test('should return correct statistics accounting for stale locks', async () => { @@ -297,46 +297,22 @@ describe('NotificationScheduler (Refactored)', () => { .build(), ); - // 2. Create a notification in the past (overdue, pending) - const overdueId = await repository.create( - NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), - ); - - // 3. Create a notification in the past that is currently PROCESSING but lock is expired + // 2. Create a notification in the past that is currently PROCESSING but lock is expired const staleId = await repository.create( NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), ); - const pastLock = NotificationFixtureBuilder.dates.past(1000); - await db.run( - `UPDATE scheduled_notifications - SET status = ?, processor_id = ?, lock_expires_at = ?, processing_started_at = ? - WHERE id = ?`, - [ - NotificationStatus.PROCESSING, - 'processor-1', - pastLock.toISOString(), - pastLock.toISOString(), - staleId, - ], - ); - - // 3. Create a notification in the past (overdue, pending) - created AFTER locking to remain in PENDING status - await repository.create( - NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), - ); - // Lock only the stale notification — fetchAndLock picks up past items by priority order, - // so we lock both past items then restore item 2 to PENDING to isolate the stale case. await repository.fetchAndLockPendingNotifications('processor-1', 30000, 10); - await db.run( - "UPDATE scheduled_notifications SET status = 'PENDING', processor_id = NULL, lock_expires_at = NULL WHERE id = ?", - [overdueId], - ); const pastLock = NotificationFixtureBuilder.dates.past(1000); await db.run('UPDATE scheduled_notifications SET lock_expires_at = ? WHERE id = ?', [ pastLock.toISOString(), staleId, ]); + // 3. Create a notification in the past (overdue, pending) - created AFTER locking to remain in PENDING status + await repository.create( + NotificationFixtureBuilder.aScheduledNotificationInput().forImmediateExecution().build(), + ); + // Get stats BEFORE recovery: // - item 1: PENDING (future) // - item 2: PENDING (restored, overdue) diff --git a/listener/src/tests/template-api-integration.test.ts b/listener/src/tests/template-api-integration.test.ts index a69856fb..46c78521 100644 --- a/listener/src/tests/template-api-integration.test.ts +++ b/listener/src/tests/template-api-integration.test.ts @@ -37,22 +37,11 @@ describe('Template API Integration Tests', () => { db = new Database(testDbPath); await db.initialize(); - // Run template schema - const schemaPath = path.join(__dirname, '../database/schema.sql'); + // Run template schema (exec handles multi-statement scripts incl. triggers) + const schemaPath = path.join(__dirname, '../database/template-schema.sql'); if (fs.existsSync(schemaPath)) { const schema = fs.readFileSync(schemaPath, 'utf-8'); - const statements = schema - .split(';') - .map((s) => s.trim()) - .filter((s) => s.length > 0 && !s.startsWith('--')); - - for (const statement of statements) { - try { - await db.run(statement); - } catch (error) { - // Ignore errors for existing tables - } - } + await db.exec(schema); } repository = new TemplateRepository(db); @@ -83,7 +72,7 @@ describe('Template API Integration Tests', () => { beforeEach(async () => { await db.run('DELETE FROM template_usage_log'); - await db.run('DELETE FROM notification_templates'); + await db.run('DELETE FROM channel_templates'); }); /** diff --git a/listener/src/tests/template-system.test.ts b/listener/src/tests/template-system.test.ts index b1a3d273..b4f69510 100644 --- a/listener/src/tests/template-system.test.ts +++ b/listener/src/tests/template-system.test.ts @@ -32,17 +32,10 @@ describe('Notification Template System', () => { db = new Database(testDbPath); await db.initialize(); - // Run template schema migration + // Run template schema (exec handles multi-statement scripts incl. triggers) const schemaPath = path.join(__dirname, '../database/template-schema.sql'); const schema = fs.readFileSync(schemaPath, 'utf-8'); - const statements = schema - .split(';') - .map((s) => s.trim()) - .filter((s) => s.length > 0); - - for (const statement of statements) { - await db.run(statement); - } + await db.exec(schema); repository = new TemplateRepository(db); service = new TemplateService(repository); @@ -57,7 +50,7 @@ describe('Notification Template System', () => { beforeEach(async () => { await db.run('DELETE FROM template_usage_log'); - await db.run('DELETE FROM notification_templates'); + await db.run('DELETE FROM channel_templates'); }); describe('TemplateRenderer', () => { diff --git a/listener/src/types/index.ts b/listener/src/types/index.ts index 835240f6..470f9ce6 100644 --- a/listener/src/types/index.ts +++ b/listener/src/types/index.ts @@ -1,5 +1,6 @@ import type { RetryFailureType } from '../services/retry-policy'; import type { CircuitBreakerConfig } from '../services/circuit-breaker'; +import type { PartialRetryBackoffConfig } from '../utils/retry-backoff-config'; import * as StellarSDK from '@stellar/stellar-sdk'; export interface NotificationProvider { @@ -100,6 +101,8 @@ export interface Config { localization?: LocalizationConfig; /** Dry-run mode: parse and validate events without persisting or delivering notifications. */ dryRun?: boolean; + /** Circuit breaker configuration for RPC failure handling. */ + circuitBreaker?: CircuitBreakerConfig; } /** @@ -269,6 +272,33 @@ export interface ExpirationConfig { * starts without a stored cursor (first boot or after downtime). */ export interface BackfillConfig { + /** + * When true, a backfill run is triggered once at startup before the live + * polling loop begins. + */ + enabled?: boolean; + /** + * The earliest ledger from which to recover missed events (inclusive). + * Events at or after this ledger that are not yet in processed_events will + * be passed through the normal processing pipeline. + */ + startLedger?: number; + /** + * The last ledger to include in the backfill window (inclusive). + * Defaults to the current network-tip ledger when not set. + */ + endLedger?: number; + /** + * Maximum number of Stellar RPC pages to fetch per contract during the + * backfill. Each page holds up to 200 events. Defaults to 50. + */ + maxPages?: number; + /** + * Upper bound on the total number of on-chain events processed per + * contract. Acts as a safety rail against unexpectedly large windows. + * Defaults to 10 000. + */ + maxEventsPerContract?: number; /** * Maximum number of ledgers to replay from the network tip on a cold start. * @@ -280,7 +310,7 @@ export interface BackfillConfig { * Set to `0` to disable the limit and allow full historical replay * (the previous default behaviour). Default: 10 000. */ - maxLedgers: number; + maxLedgers?: number; } /** @@ -297,3 +327,31 @@ export interface RpcRateLimitConfig { /** Delay in ms to apply when throttled (default: 1000). */ throttleDelayMs: number; } + +/** Configuration for fallback RPC endpoints and automatic failover. */ +export interface RpcFallbackConfig { + /** Array of fallback RPC URLs to try when primary fails */ + fallbackUrls: string[]; + /** Number of consecutive failures before marking endpoint unhealthy and failing over (default: 3) */ + failureThreshold: number; + /** Cooldown duration in ms before attempting to reuse a failed endpoint (default: 60000) */ + cooldownMs: number; + /** Timeout for RPC requests in milliseconds (default: 10000) */ + requestTimeoutMs: number; + /** Maximum number of endpoint retries for a single operation across pool (default: pool size) */ + maxRetries?: number; +} + +/** Operational status metrics for an RPC endpoint */ +export interface RpcEndpointStatus { + url: string; + isPrimary: boolean; + status: 'healthy' | 'degraded' | 'unhealthy'; + consecutiveFailures: number; + totalRequests: number; + totalSuccesses: number; + totalFailures: number; + lastFailureTime: number | null; + lastSuccessTime: number | null; + lastError: string | null; +} diff --git a/listener/src/utils/config-snapshot.ts b/listener/src/utils/config-snapshot.ts index 5c70b4d2..3252c6d9 100644 --- a/listener/src/utils/config-snapshot.ts +++ b/listener/src/utils/config-snapshot.ts @@ -5,7 +5,7 @@ * and environment configurations for operator diagnostics and troubleshooting. */ -import { redactSensitiveData, redactString } from './redact'; +import { redactString } from './redact'; export interface DiagnosticConfigSnapshot { system: { diff --git a/listener/src/utils/json-logger-formatter.ts b/listener/src/utils/json-logger-formatter.ts index 6451eaf8..6dfd1d1f 100644 --- a/listener/src/utils/json-logger-formatter.ts +++ b/listener/src/utils/json-logger-formatter.ts @@ -6,7 +6,7 @@ */ import winston from 'winston'; -import { redactSensitiveData, redactString } from './redact'; +import { redactObject, redactValue, redactString } from './redact'; export interface StructuredLogRecord { timestamp: string; @@ -39,7 +39,7 @@ export function createStructuredJsonFormat(options: JsonFormatterOptions = {}) { const sanitizedMessage = typeof message === 'string' ? redactString(message) : String(message); // Sanitize extra metadata - const sanitizedMetadata = redactSensitiveData(rest) as Record; + const sanitizedMetadata = redactObject(rest) as Record; const record: StructuredLogRecord = { timestamp: (timestamp as string) || new Date().toISOString(), @@ -49,7 +49,7 @@ export function createStructuredJsonFormat(options: JsonFormatterOptions = {}) { environment, ...(requestId ? { requestId: String(requestId) } : {}), ...(durationMs !== undefined ? { durationMs: Number(durationMs) } : {}), - ...(error ? { error: redactSensitiveData(error) as Record } : {}), + ...(error ? { error: redactValue(error) as Record } : {}), ...sanitizedMetadata, }; diff --git a/listener/src/utils/response.ts b/listener/src/utils/response.ts index 85fbcb9d..7d426d68 100644 --- a/listener/src/utils/response.ts +++ b/listener/src/utils/response.ts @@ -106,7 +106,6 @@ export function err( * Write a raw JSON response. Prefer `sendOk` / `sendErr` over this. */ export function sendJson(res: http.ServerResponse, statusCode: number, body: unknown): void { - res.setHeader('Content-Type', 'application/json'); res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body)); }