diff --git a/contracts/README.md b/contracts/README.md index db4689f1..beeb1232 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -47,11 +47,12 @@ create_commitment ──► fund_escrow ──► release (matured: p | --- | --- | | `initialize(admin, token, fee_recipient, safe_default_penalty_bps, balanced_default_penalty_bps, aggressive_default_penalty_bps)` | One-time setup of admin, escrow token (SAC), fee recipient, and default penalties for each risk profile. | | `create_commitment(owner, asset, amount, risk, duration_days, penalty_bps)` | Create an unfunded commitment with explicit penalty; returns its `id`. | -| `create_commitment_with_default_penalty(owner, asset, amount, risk, duration_days)` | Create an unfunded commitment using the default penalty for the risk profile; returns its `id`. | +| `create_commitment_default(owner, asset, amount, risk, duration_days)` | Create an unfunded commitment using the default penalty for the risk profile; returns its `id`. | | `fund_escrow(commitment_id)` | Transfer `amount` from owner into the contract (`Created → Funded`). | | `deposit_yield_pool(admin, amount)` | Admin-only deposit of yield tokens into the contract yield pool. | | `get_yield_pool_balance()` | Read the yield pool balance available for matured release payouts. | | `release(commitment_id, caller)` | Return principal plus accrued yield to owner once matured (`Funded → Released`). | +| `settle_commitment(commitment_id, caller)` | Alias for `release` that returns a settlement result matching backend ABI expectations. | | `refund(commitment_id)` | Early-exit refund of principal minus `penalty_bps` (`Funded → Refunded`). | | `dispute(commitment_id, caller, reason)` | Freeze a funded commitment pending admin resolution. The reason is automatically categorized. | | `resolve_dispute(commitment_id, release_to_owner)` | Admin-only settlement of a disputed commitment. | @@ -125,7 +126,7 @@ recipient on `refund` / adverse `resolve_dispute`. ### Default penalties per risk profile Default penalties are configured once at initialization and automatically applied -to commitments created via `create_commitment_with_default_penalty()`. This +to commitments created via `create_commitment_default()`. This simplifies commitment creation when consistent penalty tiers are desired. #### Backend-aligned defaults @@ -147,7 +148,7 @@ The contract provides two ways to create commitments: - Overrides default if needed - Useful for custom deal terms -2. **Default penalty** (`create_commitment_with_default_penalty`): Use the profile default +2. **Default penalty** (`create_commitment_default`): Use the profile default - Simplifies API calls - Ensures consistency across commitments - No penalty parameter needed @@ -155,7 +156,7 @@ The contract provides two ways to create commitments: Example: ```rust // Use default penalty (e.g., 3% for Balanced risk) -let id = contract.create_commitment_with_default_penalty( +let id = contract.create_commitment_default( &owner, &asset, &1000, &RiskProfile::Balanced, &30 )?; diff --git a/contracts/escrow/cargo_test_output.txt b/contracts/escrow/cargo_test_output.txt new file mode 100644 index 00000000..7d4afa39 --- /dev/null +++ b/contracts/escrow/cargo_test_output.txt @@ -0,0 +1,333 @@ + Compiling commitlabs-escrow v0.1.0 (/workspaces/Commitlabs-Frontend/contracts/escrow) +error: an inner attribute is not permitted in this context + --> escrow/src/test.rs:37:1 + | +37 | #![cfg(test)] + | ^^^^^^^^^^^^^ +38 | +39 | use super::*; + | ------------- the inner attribute doesn't annotate this `use` import + | + = note: inner attributes, like `#![no_std]`, annotate the item enclosing them, and are usually found at the beginning of source files +help: to annotate the `use` import, change the attribute from inner to outer style + | +37 - #![cfg(test)] +37 + #[cfg(test)] + | + +error: contract function name is too long: 38, max is 32 + --> escrow/src/lib.rs:350:12 + | +350 | pub fn create_commitment_with_default_penalty( + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0592]: duplicate definitions with name `is_paused` + --> escrow/src/lib.rs:786:5 + | +268 | pub fn is_paused(env: Env) -> bool { + | ---------------------------------- other definition for `is_paused` +... +786 | fn is_paused(env: &Env) -> bool { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definitions for `is_paused` + +error[E0599]: no variant, associated function, or constant named `Paused` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:246:48 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Paused` not found for this enum +... +246 | env.storage().instance().set(&DataKey::Paused, &true); + | ^^^^^^ variant, associated function, or constant not found in `DataKey` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:248:14 + | +248 | .publish((Symbol::new(&env, "pause"), admin), ()); + | ^^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + +error[E0599]: no variant, associated function, or constant named `Paused` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:261:48 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Paused` not found for this enum +... +261 | env.storage().instance().set(&DataKey::Paused, &false); + | ^^^^^^ variant, associated function, or constant not found in `DataKey` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:263:14 + | +263 | .publish((Symbol::new(&env, "unpause"), admin), ()); + | ^^^^^^^ + +error[E0599]: no variant, associated function, or constant named `Paused` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:271:28 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Paused` not found for this enum +... +271 | .get(&DataKey::Paused) + | ^^^^^^ variant, associated function, or constant not found in `DataKey` + +error[E0063]: missing field `accrued_yield` in initializer of `Commitment` + --> escrow/src/lib.rs:316:26 + | +316 | let commitment = Commitment { + | ^^^^^^^^^^ missing `accrued_yield` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:334:22 + | +334 | env.events().publish( + | ^^^^^^^ + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:395:22 + | +395 | env.events().publish( + | ^^^^^^^ + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:420:22 + | +420 | env.events().publish( + | ^^^^^^^ + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:443:22 + | +443 | env.events().publish( + | ^^^^^^^ + +error[E0599]: no variant, associated function, or constant named `InsufficientYieldPool` found for enum `Error` in the current scope + --> escrow/src/lib.rs:468:31 + | +127 | pub enum Error { + | -------------- variant, associated function, or constant `InsufficientYieldPool` not found for this enum +... +468 | return Err(Error::InsufficientYieldPool); + | ^^^^^^^^^^^^^^^^^^^^^ variant, associated function, or constant not found in `Error` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:480:22 + | +480 | env.events().publish( + | ^^^^^^^ + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:582:22 + | +582 | env.events().publish( + | ^^^^^^^ + +error[E0599]: no variant, associated function, or constant named `InsufficientYieldPool` found for enum `Error` in the current scope + --> escrow/src/lib.rs:617:39 + | +127 | pub enum Error { + | -------------- variant, associated function, or constant `InsufficientYieldPool` not found for this enum +... +617 | return Err(Error::InsufficientYieldPool); + | ^^^^^^^^^^^^^^^^^^^^^ variant, associated function, or constant not found in `Error` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:633:22 + | +633 | env.events().publish( + | ^^^^^^^ + +error[E0599]: no variant, associated function, or constant named `Attestations` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:658:28 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Attestations` not found for this enum +... +658 | .get(&DataKey::Attestations(commitment_id)) + | ^^^^^^^^^^^^ variant, associated function, or constant not found in `DataKey` + +error[E0599]: no variant, associated function, or constant named `Attestations` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:669:28 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Attestations` not found for this enum +... +669 | .set(&DataKey::Attestations(commitment_id), &attestations); + | ^^^^^^^^^^^^ variant, associated function, or constant not found in `DataKey` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:671:22 + | +671 | env.events().publish( + | ^^^^^^^ + +error[E0599]: no variant, associated function, or constant named `Attestations` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:687:28 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Attestations` not found for this enum +... +687 | .get(&DataKey::Attestations(commitment_id)) + | ^^^^^^^^^^^^ variant, associated function, or constant not found in `DataKey` + +warning: use of deprecated method `soroban_sdk::events::Events::publish`: use the #[contractevent] macro on a contract event type + --> escrow/src/lib.rs:753:22 + | +753 | env.events().publish( + | ^^^^^^^ + +error[E0599]: no variant, associated function, or constant named `Paused` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:789:28 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `Paused` not found for this enum +... +789 | .get(&DataKey::Paused) + | ^^^^^^ variant, associated function, or constant not found in `DataKey` + +error[E0308]: mismatched types + --> escrow/src/lib.rs:794:28 + | +794 | if Self::is_paused(env) { + | --------------- ^^^ expected `Env`, found `&Env` + | | + | arguments to this function are incorrect + | +note: associated function defined here + --> escrow/src/lib.rs:268:12 + | +268 | pub fn is_paused(env: Env) -> bool { + | ^^^^^^^^^ -------- +help: consider using clone here + | +794 | if Self::is_paused(env.clone()) { + | ++++++++ + +error[E0599]: no variant, associated function, or constant named `YieldPool` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:828:28 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `YieldPool` not found for this enum +... +828 | .get(&DataKey::YieldPool) + | ^^^^^^^^^ variant, associated function, or constant not found in `DataKey` + +error[E0599]: no variant, associated function, or constant named `YieldPool` found for enum `DataKey` in the current scope + --> escrow/src/lib.rs:833:48 + | + 25 | pub enum DataKey { + | ---------------- variant, associated function, or constant `YieldPool` not found for this enum +... +833 | env.storage().instance().set(&DataKey::YieldPool, &amount); + | ^^^^^^^^^ variant, associated function, or constant not found in `DataKey` + +error[E0599]: no method named `to_lowercase` found for reference `&soroban_sdk::String` in the current scope + --> escrow/src/lib.rs:848:35 + | +848 | let reason_lower = reason.to_lowercase(); + | ^^^^^^^^^^^^ method not found in `&soroban_sdk::String` + +error[E0308]: mismatched types + --> escrow/src/test.rs:8:23 + | +8 | f.env.set_auths(&[&f.admin]); + | ^^^^^^^^ expected `SorobanAuthorizationEntry`, found `&Address` + +error[E0599]: no method named `set_admin` found for struct `EscrowContractClient<'a>` in the current scope + --> escrow/src/test.rs:9:14 + | + 9 | f.client.set_admin(&new_admin); + | ^^^^^^^^^ method not found in `EscrowContractClient<'_>` + | + ::: escrow/src/lib.rs:177:1 + | +177 | #[contract] + | ----------- method `set_admin` not found for this struct + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `set_admin`, perhaps you need to implement it: + candidate #1: `StellarAssetInterface` + +error[E0308]: mismatched types + --> escrow/src/test.rs:11:23 + | +11 | f.env.set_auths(&[&new_admin]); + | ^^^^^^^^^^ expected `SorobanAuthorizationEntry`, found `&Address` + +error[E0599]: no method named `set_fee_recipient` found for struct `EscrowContractClient<'a>` in the current scope + --> escrow/src/test.rs:12:14 + | + 12 | f.client.set_fee_recipient(&new_fee); + | ^^^^^^^^^^^^^^^^^ method not found in `EscrowContractClient<'_>` + | + ::: escrow/src/lib.rs:177:1 + | +177 | #[contract] + | ----------- method `set_fee_recipient` not found for this struct + +error[E0308]: mismatched types + --> escrow/src/test.rs:29:23 + | +29 | f.env.set_auths(&[¬_admin]); + | ^^^^^^^^^^ expected `SorobanAuthorizationEntry`, found `&Address` + +error[E0599]: no method named `try_set_admin` found for struct `EscrowContractClient<'a>` in the current scope + --> escrow/src/test.rs:30:24 + | + 30 | let res = f.client.try_set_admin(&new_admin); + | ^^^^^^^^^^^^^ method not found in `EscrowContractClient<'_>` + | + ::: escrow/src/lib.rs:177:1 + | +177 | #[contract] + | ----------- method `try_set_admin` not found for this struct + +error[E0599]: no method named `try_set_fee_recipient` found for struct `EscrowContractClient<'a>` in the current scope + --> escrow/src/test.rs:34:25 + | + 34 | let res2 = f.client.try_set_fee_recipient(&new_fee); + | ^^^^^^^^^^^^^^^^^^^^^ method not found in `EscrowContractClient<'_>` + | + ::: escrow/src/lib.rs:177:1 + | +177 | #[contract] + | ----------- method `try_set_fee_recipient` not found for this struct + +error[E0061]: this method takes 6 arguments but 3 arguments were supplied + --> escrow/src/test.rs:110:10 + | +110 | .try_initialize(&f.admin, &f.asset, &other); + | ^^^^^^^^^^^^^^---------------------------- three arguments of type `&u32`, `&u32`, and `&u32` are missing + | +note: method defined here + --> escrow/src/lib.rs:194:12 + | +194 | pub fn initialize( + | ^^^^^^^^^^ +... +199 | safe_default_penalty_bps: u32, + | ----------------------------- +200 | balanced_default_penalty_bps: u32, + | --------------------------------- +201 | aggressive_default_penalty_bps: u32, + | ----------------------------------- +help: provide the arguments + | +110 | .try_initialize(&f.admin, &f.asset, &other, /* &u32 */, /* &u32 */, /* &u32 */); + | ++++++++++++++++++++++++++++++++++++ + +error[E0599]: no variant, associated function, or constant named `InsufficientYieldPool` found for enum `Error` in the current scope + --> escrow/src/test.rs:209:35 + | +209 | assert_eq!(res, Err(Ok(Error::InsufficientYieldPool))); + | ^^^^^^^^^^^^^^^^^^^^^ variant, associated function, or constant not found in `Error` + | + ::: escrow/src/lib.rs:127:1 + | +127 | pub enum Error { + | -------------- variant, associated function, or constant `InsufficientYieldPool` not found for this enum + +Some errors have detailed explanations: E0061, E0063, E0308, E0592, E0599. +For more information about an error, try `rustc --explain E0061`. +warning: `commitlabs-escrow` (lib test) generated 11 warnings +error: could not compile `commitlabs-escrow` (lib test) due to 26 previous errors; 11 warnings emitted diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 4083c3d0..9dbfdbd6 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -35,6 +35,12 @@ pub enum DataKey { OwnerIndex(Address), /// Protocol fee recipient. FeeRecipient, + /// Yield pool balance used to pay matured release yield. + YieldPool, + /// Contract pause flag to halt write operations. + Paused, + /// Attestation history for a commitment. + Attestations(u64), /// Dispute record for a commitment, keyed by commitment id. Dispute(u64), /// Default penalty in basis points for each RiskProfile. @@ -134,8 +140,10 @@ pub enum Error { NotMatured = 7, InvalidDuration = 8, PenaltyTooHigh = 9, + /// Insufficient funds in the yield pool to satisfy a matured release. + InsufficientYieldPool = 10, /// Contract is currently paused for emergency halt. - Paused = 10, + Paused = 11, } /// Result of an early exit commitment. @@ -148,6 +156,23 @@ pub struct EarlyExitResult { pub finalStatus: EscrowStatus, } +/// Result of a matured settlement invoked by `settle_commitment`. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(non_snake_case)] +pub struct SettlementResult { + pub settlementAmount: i128, + pub finalStatus: String, +} + +#[contracttype] +#[derive(Clone)] +pub struct AttestationRecord { + pub attestor: Address, + pub compliance_score: u32, + pub timestamp: u64, +} + const MAX_PENALTY_BPS: u32 = 10_000; const SECONDS_PER_DAY: u64 = 86_400; const YIELD_BPS_DENOMINATOR: i128 = 3_650_000; // 365 days * 10_000 bps @@ -309,6 +334,7 @@ impl EscrowContract { owner: owner.clone(), asset, amount, + accrued_yield: calculate_accrued_yield(amount, duration_days, risk), risk, status: EscrowStatus::Created, maturity, @@ -338,7 +364,7 @@ impl EscrowContract { /// /// `duration_days` is converted to an absolute maturity timestamp using the /// current ledger time. - pub fn create_commitment_with_default_penalty( + pub fn create_commitment_default( env: Env, owner: Address, asset: Address, @@ -347,6 +373,7 @@ impl EscrowContract { duration_days: u32, ) -> Result { Self::require_init(&env)?; + Self::require_not_paused(&env)?; owner.require_auth(); if amount <= 0 { @@ -361,7 +388,10 @@ impl EscrowContract { let id = Self::next_id(&env); let now = env.ledger().timestamp(); - let maturity = now + (duration_days as u64) * SECONDS_PER_DAY; + let duration_seconds = (duration_days as u64) + .checked_mul(SECONDS_PER_DAY) + .ok_or(Error::InvalidDuration)?; + let maturity = now.checked_add(duration_seconds).ok_or(Error::InvalidDuration)?; let accrued_yield = calculate_accrued_yield(amount, duration_days, risk); let commitment = Commitment { @@ -443,12 +473,9 @@ impl EscrowContract { Self::yield_pool_balance(&env) } - /// Release the escrowed funds back to the owner once the commitment has - /// matured. Only callable on a `Funded` commitment at/after maturity. - pub fn release(env: Env, commitment_id: u64, caller: Address) -> Result { - Self::require_init(&env)?; + fn perform_release(env: &Env, commitment_id: u64, caller: &Address) -> Result { caller.require_auth(); - let mut c = Self::load(&env, commitment_id)?; + let mut c = Self::load(env, commitment_id)?; if c.status != EscrowStatus::Funded { return Err(Error::InvalidState); @@ -457,27 +484,50 @@ impl EscrowContract { return Err(Error::NotMatured); } - let yield_pool = Self::yield_pool_balance(&env); + let yield_pool = Self::yield_pool_balance(env); if yield_pool < c.accrued_yield { return Err(Error::InsufficientYieldPool); } let total_payout = c.amount + c.accrued_yield; - let token = Self::token_client(&env); + let token = Self::token_client(env); let contract = env.current_contract_address(); token.transfer(&contract, &c.owner, &total_payout); - Self::set_yield_pool_balance(&env, yield_pool - c.accrued_yield); + Self::set_yield_pool_balance(env, yield_pool - c.accrued_yield); c.status = EscrowStatus::Released; - Self::save(&env, &c); + Self::save(env, &c); env.events().publish( - (Symbol::new(&env, "release"), c.owner.clone()), + (Symbol::new(env, "release"), c.owner.clone()), (commitment_id, total_payout, c.accrued_yield), ); Ok(total_payout) } + /// Release the escrowed funds back to the owner once the commitment has + /// matured. Only callable on a `Funded` commitment at/after maturity. + pub fn release(env: Env, commitment_id: u64, caller: Address) -> Result { + Self::require_init(&env)?; + Self::perform_release(&env, commitment_id, &caller) + } + + /// Alias matching the backend settlement method name. + /// Delegates to matured release logic and returns a structured settlement + /// result the backend can parse. + pub fn settle_commitment( + env: Env, + commitment_id: u64, + caller: Address, + ) -> Result { + Self::require_init(&env)?; + let payout = Self::perform_release(&env, commitment_id, &caller)?; + Ok(SettlementResult { + settlementAmount: payout, + finalStatus: String::from_str(&env, "SETTLED"), + }) + } + /// Early-exit refund. Returns the principal minus the early-exit penalty; /// the penalty is sent to the fee recipient. Only the owner may refund and /// only while the commitment is `Funded` and before maturity. @@ -677,7 +727,7 @@ impl EscrowContract { /// Retrieve the default penalty (in basis points) for a specific risk profile. /// Configured at initialization time and used by - /// `create_commitment_with_default_penalty()`. Useful for querying the + /// `create_commitment_default()`. Useful for querying the /// current penalty configuration. pub fn get_default_penalty(env: Env, risk: RiskProfile) -> Result { env.storage() @@ -754,7 +804,7 @@ impl EscrowContract { id } - fn is_paused(env: &Env) -> bool { + fn is_paused_internal(env: &Env) -> bool { env.storage() .instance() .get(&DataKey::Paused) @@ -762,7 +812,7 @@ impl EscrowContract { } fn require_not_paused(env: &Env) -> Result<(), Error> { - if Self::is_paused(env) { + if Self::is_paused_internal(env) { return Err(Error::Paused); } Ok(()) diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index 22eaef23..6563b8d8 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -1,3 +1,5 @@ +#![cfg(test)] + #[test] fn admin_can_rotate_admin_and_fee_recipient() { let f = setup(); @@ -34,7 +36,6 @@ fn unauthorized_cannot_rotate_admin_or_fee_recipient() { let res2 = f.client.try_set_fee_recipient(&new_fee); assert_eq!(res2, Err(Ok(Error::Unauthorized))); } -#![cfg(test)] use super::*; use soroban_sdk::{ @@ -156,6 +157,44 @@ fn release_after_maturity_pays_principal_plus_yield() { assert_eq!(commitment.status, EscrowStatus::Released); } +#[test] +fn settle_commitment_alias_matches_release_and_returns_settlement_result() { + let f = setup(); + let owner = Address::generate(&f.env); + fund_owner(&f, &owner, 1_000); + let id = f + .client + .create_commitment(&owner, &f.asset, &1_000, &RiskProfile::Safe, &10, &200); + f.client.fund_escrow(&id); + + let admin_deposit = 10; + f.token_admin.mint(&f.admin, &admin_deposit); + f.client.deposit_yield_pool(&f.admin, &admin_deposit); + + // Advance ledger time past maturity. + f.env.ledger().set_timestamp(11 * 86_400); + let result = f.client.settle_commitment(&id, &owner); + + assert_eq!(result.settlementAmount, 1_001); + assert_eq!(result.finalStatus, String::from_str(&f.env, "SETTLED")); + assert_eq!(f.token.balance(&owner), 1_001); + assert_eq!(f.client.get_commitment(&id).status, EscrowStatus::Released); +} + +#[test] +fn settle_commitment_before_maturity_fails() { + let f = setup(); + let owner = Address::generate(&f.env); + fund_owner(&f, &owner, 1_000); + let id = f + .client + .create_commitment(&owner, &f.asset, &1_000, &RiskProfile::Safe, &10, &200); + f.client.fund_escrow(&id); + + let res = f.client.try_settle_commitment(&id, &owner); + assert_eq!(res, Err(Ok(Error::NotMatured))); +} + #[test] fn release_without_yield_pool_fails() { let f = setup(); @@ -587,13 +626,13 @@ fn get_default_penalty_returns_configured_values() { } #[test] -fn create_commitment_with_default_penalty_safe() { +fn create_commitment_default_safe() { let f = setup(); let owner = Address::generate(&f.env); fund_owner(&f, &owner, 1_000); // Create with default penalty for Safe profile (2%). - let id = f.client.create_commitment_with_default_penalty( + let id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -607,13 +646,13 @@ fn create_commitment_with_default_penalty_safe() { } #[test] -fn create_commitment_with_default_penalty_balanced() { +fn create_commitment_default_balanced() { let f = setup(); let owner = Address::generate(&f.env); fund_owner(&f, &owner, 1_000); // Create with default penalty for Balanced profile (3%). - let id = f.client.create_commitment_with_default_penalty( + let id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -627,13 +666,13 @@ fn create_commitment_with_default_penalty_balanced() { } #[test] -fn create_commitment_with_default_penalty_aggressive() { +fn create_commitment_default_aggressive() { let f = setup(); let owner = Address::generate(&f.env); fund_owner(&f, &owner, 1_000); // Create with default penalty for Aggressive profile (5%). - let id = f.client.create_commitment_with_default_penalty( + let id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -675,7 +714,7 @@ fn refund_with_default_penalty_safe_applies_correct_fee() { fund_owner(&f, &owner, 1_000); // Create commitment with Safe default penalty (2%). - let id = f.client.create_commitment_with_default_penalty( + let id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -697,7 +736,7 @@ fn refund_with_default_penalty_balanced_applies_correct_fee() { fund_owner(&f, &owner, 1_000); // Create commitment with Balanced default penalty (3%). - let id = f.client.create_commitment_with_default_penalty( + let id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -719,7 +758,7 @@ fn refund_with_default_penalty_aggressive_applies_correct_fee() { fund_owner(&f, &owner, 1_000); // Create commitment with Aggressive default penalty (5%). - let id = f.client.create_commitment_with_default_penalty( + let id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -741,7 +780,7 @@ fn multiple_commitments_different_profiles_use_correct_defaults() { fund_owner(&f, &owner, 10_000); // Create three commitments with different risk profiles. - let safe_id = f.client.create_commitment_with_default_penalty( + let safe_id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -749,7 +788,7 @@ fn multiple_commitments_different_profiles_use_correct_defaults() { &30, ); - let balanced_id = f.client.create_commitment_with_default_penalty( + let balanced_id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -757,7 +796,7 @@ fn multiple_commitments_different_profiles_use_correct_defaults() { &30, ); - let aggressive_id = f.client.create_commitment_with_default_penalty( + let aggressive_id = f.client.create_commitment_default( &owner, &f.asset, &1_000, @@ -780,7 +819,7 @@ fn create_commitment_with_default_validates_amount() { let owner = Address::generate(&f.env); // Attempt to create with invalid amount. - let res = f.client.try_create_commitment_with_default_penalty( + let res = f.client.try_create_commitment_default( &owner, &f.asset, &0, // Invalid: amount must be > 0 @@ -797,7 +836,7 @@ fn create_commitment_with_default_validates_duration() { fund_owner(&f, &owner, 1_000); // Attempt to create with invalid duration. - let res = f.client.try_create_commitment_with_default_penalty( + let res = f.client.try_create_commitment_default( &owner, &f.asset, &1_000, diff --git a/contracts/target/.rustc_info.json b/contracts/target/.rustc_info.json index 17a859d2..38dbf4c2 100644 --- a/contracts/target/.rustc_info.json +++ b/contracts/target/.rustc_info.json @@ -1 +1 @@ -{"rustc_fingerprint":15130695044876549716,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.95.0 (59807616e 2026-04-14)\nbinary: rustc\ncommit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860\ncommit-date: 2026-04-14\nhost: x86_64-pc-windows-msvc\nrelease: 1.95.0\nLLVM version: 22.1.2\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Dell\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""}},"successes":{}} \ No newline at end of file +{"rustc_fingerprint":2502430289109187017,"outputs":{"14249696209439168977":{"success":true,"status":"","code":0,"stdout":"rustc 1.98.0-nightly (d1fc603d1 2026-05-26)\nbinary: rustc\ncommit-hash: d1fc603d1788cc3c0eebdb94a45a61c4f33b1674\ncommit-date: 2026-05-26\nhost: x86_64-unknown-linux-gnu\nrelease: 1.98.0-nightly\nLLVM version: 22.1.6\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/codespace/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nemscripten_wasm_eh\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"x87\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_has_atomic_primitive_alignment=\"16\"\ntarget_has_atomic_primitive_alignment=\"32\"\ntarget_has_atomic_primitive_alignment=\"64\"\ntarget_has_atomic_primitive_alignment=\"8\"\ntarget_has_atomic_primitive_alignment=\"ptr\"\ntarget_has_reliable_f128\ntarget_has_reliable_f16\ntarget_has_reliable_f16_math\ntarget_object_format=\"elf\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file diff --git a/docs/backend-error-codes.md b/docs/backend-error-codes.md index 3531e340..c721ad3a 100644 --- a/docs/backend-error-codes.md +++ b/docs/backend-error-codes.md @@ -520,6 +520,8 @@ showBlockchainError(response.error.details); // Usually not retriable — user must fix underlying issue ``` +The backend centralizes blockchain error normalization in `src/lib/backend/errors.ts` so contract invocation failures are classified consistently across the service layer. + **Retriable**: ❌ No --- diff --git a/src/lib/backend/errors.test.ts b/src/lib/backend/errors.test.ts index 1e99e051..b45f167e 100644 --- a/src/lib/backend/errors.test.ts +++ b/src/lib/backend/errors.test.ts @@ -10,6 +10,8 @@ import { NotFoundError, ConflictError, InternalError, + BackendError, + normalizeBackendError, HTTP_ERROR_CODES, } from './errors'; @@ -134,3 +136,52 @@ describe('HTTP_ERROR_CODES', () => { expect(HTTP_ERROR_CODES[504]).toBe('GATEWAY_TIMEOUT'); }); }); + +describe('normalizeBackendError', () => { + it('should classify timeout errors as GATEWAY_TIMEOUT and retryable', () => { + const normalized = normalizeBackendError(new Error('RPC Timeout'), { + code: 'BLOCKCHAIN_CALL_FAILED', + message: 'Fallback message', + status: 502, + }); + + expect(normalized).toBeInstanceOf(BackendError); + expect(normalized.code).toBe('GATEWAY_TIMEOUT'); + expect(normalized.status).toBe(504); + expect(normalized.message).toContain('timed out'); + expect(normalized.details).toEqual({ retryable: true }); + }); + + it('should classify rate-limit errors as TOO_MANY_REQUESTS and retryable', () => { + const normalized = normalizeBackendError(new Error('429 Too Many Requests'), { + code: 'BLOCKCHAIN_CALL_FAILED', + message: 'Fallback message', + status: 502, + }); + + expect(normalized.code).toBe('TOO_MANY_REQUESTS'); + expect(normalized.status).toBe(429); + expect(normalized.details).toEqual({ retryable: true }); + }); + + it('should preserve fallback details and add retryable flag for existing BackendError status', () => { + const original = new BackendError({ + code: 'TOO_MANY_REQUESTS', + message: 'Rate limited', + status: 429, + details: { limit: 100 }, + }); + + const normalized = normalizeBackendError(original, { + code: 'BLOCKCHAIN_CALL_FAILED', + message: 'Fallback message', + status: 502, + details: { method: 'test' }, + }); + + expect(normalized).toBeInstanceOf(BackendError); + expect(normalized.code).toBe('TOO_MANY_REQUESTS'); + expect(normalized.status).toBe(429); + expect(normalized.details).toEqual({ limit: 100, method: 'test', retryable: true }); + }); +}); diff --git a/src/lib/backend/errors.ts b/src/lib/backend/errors.ts index c9fb0609..ded16420 100644 --- a/src/lib/backend/errors.ts +++ b/src/lib/backend/errors.ts @@ -206,14 +206,140 @@ export function isBackendError(value: unknown): value is BackendError { return value instanceof BackendError; } +function asRecord(value: unknown): Record { + return value && typeof value === "object" + ? (value as Record) + : {}; +} + +function isRetryableStatus(status: number): boolean { + return [429, 503, 504].includes(status); +} + +function classifyBackendError( + error: unknown, +): + | { + code: BackendErrorCode; + status: number; + message: string; + retryable: boolean; + } + | undefined { + const errMessage = error instanceof Error ? error.message : String(error); + const errStr = errMessage.toLowerCase(); + + if ( + errStr.includes("timeout") || + errStr.includes("deadline") || + errStr.includes("timed out") + ) { + return { + code: "GATEWAY_TIMEOUT", + status: 504, + message: + "The blockchain operation timed out. It may still be processed later.", + retryable: true, + }; + } + + if ( + errStr.includes("429") || + errStr.includes("rate limit") || + errStr.includes("too many requests") + ) { + return { + code: "TOO_MANY_REQUESTS", + status: 429, + message: "Rate limit exceeded for blockchain calls. Please try again later.", + retryable: true, + }; + } + + if ( + errStr.includes("503") || + errStr.includes("service unavailable") || + errStr.includes("temporarily unavailable") + ) { + return { + code: "SERVICE_UNAVAILABLE", + status: 503, + message: "Blockchain service is temporarily unavailable. Please try again later.", + retryable: true, + }; + } + + if (errStr.includes("not found") || errStr.includes("404")) { + return { + code: "NOT_FOUND", + status: 404, + message: "The requested resource was not found on the blockchain.", + retryable: false, + }; + } + + if ( + errStr.includes("insufficient") || + errStr.includes("invalid") || + errStr.includes("malformed") + ) { + return { + code: "VALIDATION_ERROR", + status: 400, + message: + "The transaction was rejected due to invalid parameters or state.", + retryable: false, + }; + } + + return undefined; +} + export function normalizeBackendError( error: unknown, fallback: Omit, ): BackendError { if (isBackendError(error)) { - return error; + const details = { + ...asRecord(error.details), + ...asRecord(fallback.details), + }; + const retryable = + asRecord(error.details).retryable === true || + isRetryableStatus(error.status); + + return new BackendError({ + code: error.code, + message: error.message, + status: error.status, + details: { + ...details, + retryable, + }, + cause: error, + }); } - return new BackendError({ ...fallback, cause: error }); + + const classified = + fallback.code === "BLOCKCHAIN_CALL_FAILED" + ? classifyBackendError(error) + : undefined; + + const status = classified?.status ?? fallback.status; + const code = classified?.code ?? fallback.code; + const message = classified?.message ?? fallback.message; + const retryable = classified?.retryable ?? isRetryableStatus(fallback.status); + + return new BackendError({ + code, + message, + status, + details: { + ...asRecord(fallback.details), + retryable, + }, + cause: error, + }); } export function toBackendErrorResponse( diff --git a/src/lib/backend/services/contracts.ts b/src/lib/backend/services/contracts.ts index 298e3f5c..085a337b 100644 --- a/src/lib/backend/services/contracts.ts +++ b/src/lib/backend/services/contracts.ts @@ -181,87 +181,6 @@ function normalizeStatus(value: unknown): ChainCommitmentStatus { * Maps RPC failures, simulation errors, and timeouts to appropriate status codes. * Ensures that sensitive raw RPC details are not leaked to the client. */ -function normalizeContractError( - error: unknown, - defaults: { - code: BackendErrorCode; - message: string; - status: number; - details?: Record; - }, -): BackendError { - // If it's already a well-formed BackendError, we enrich it with defaults - if (error instanceof BackendError) { - const isRetryable = [429, 503, 504].includes(error.status); - return new BackendError({ - code: error.code, - message: error.message, - status: error.status, - details: { - ...asRecord(error.details), - ...asRecord(defaults.details), - retryable: isRetryable || asRecord(error.details).retryable === true, - }, - }); - } - - const errMessage = error instanceof Error ? error.message : String(error); - const errStr = errMessage.toLowerCase(); - - let status = defaults.status; - let code = defaults.code; - let message = defaults.message; - let retryable = false; - - // Pattern match for specific failure types from Soroban RPC or SDK - if ( - errStr.includes("timeout") || - errStr.includes("deadline") || - errStr.includes("timed out") - ) { - status = 504; - code = "GATEWAY_TIMEOUT"; - message = - "The blockchain operation timed out. It may still be processed later."; - retryable = true; - } else if ( - errStr.includes("429") || - errStr.includes("rate limit") || - errStr.includes("too many requests") - ) { - status = 429; - code = "TOO_MANY_REQUESTS"; - message = - "Rate limit exceeded for blockchain calls. Please try again later."; - retryable = true; - } else if (errStr.includes("not found") || errStr.includes("404")) { - status = 404; - code = "NOT_FOUND"; - message = "The requested resource was not found on the blockchain."; - } else if ( - errStr.includes("insufficient") || - errStr.includes("invalid") || - errStr.includes("malformed") - ) { - status = 400; - code = "VALIDATION_ERROR"; - message = - "The transaction was rejected due to invalid parameters or state."; - } else if (status >= 500) { - retryable = true; - } - - return new BackendError({ - code, - message, - status, - details: { - ...asRecord(defaults.details), - retryable, - }, - }); -} - function parseChainCommitment(value: unknown): ChainCommitment { const raw = asRecord(value); const id = asString(raw.id ?? raw.commitmentId); @@ -374,7 +293,7 @@ async function waitForTransactionResult( return tx.returnValue ? scValToNative(tx.returnValue) : null; } if (tx.status === SorobanRpc.Api.GetTransactionStatus.FAILED) { - throw normalizeContractError(new Error("Transaction execution failed"), { + throw normalizeBackendError(new Error("Transaction execution failed"), { code: "BLOCKCHAIN_CALL_FAILED", message: "Soroban transaction failed.", status: 502, @@ -387,7 +306,7 @@ async function waitForTransactionResult( }); } - throw normalizeContractError(new Error("RPC Timeout"), { + throw normalizeBackendError(new Error("RPC Timeout"), { code: "BLOCKCHAIN_CALL_FAILED", message: "Timed out waiting for Soroban transaction result.", status: 504, @@ -441,7 +360,7 @@ async function invokeContractMethod( const simulation = await server.simulateTransaction(tx); if (SorobanRpc.Api.isSimulationError(simulation)) { - throw normalizeContractError(new Error(simulation.error), { + throw normalizeBackendError(new Error(simulation.error), { code: "BLOCKCHAIN_CALL_FAILED", message: `Soroban simulation failed for ${methodName}.`, status: 502, @@ -633,7 +552,7 @@ export async function getUserCommitmentsFromChain( const countersAdapter = getCountersAdapter(); void countersAdapter.incrementChainFailures(); - throw normalizeContractError(error, { + throw normalizeBackendError(error, { code: "BLOCKCHAIN_CALL_FAILED", message: "Unable to fetch user commitments from chain.", status: 502, @@ -755,6 +674,9 @@ export async function settleCommitmentOnChain( ], "write", ); + // This method is intentionally aligned with the escrow contract alias in + // contracts/escrow/src/lib.rs so the backend can invoke settled releases + // using the expected ABI shape. // Increment successful actions counter on successful settlement const countersAdapter = getCountersAdapter(); @@ -783,7 +705,7 @@ export async function settleCommitmentOnChain( const countersAdapter = getCountersAdapter(); void countersAdapter.incrementChainFailures(); // Fire and forget for metrics - throw normalizeContractError(error, { + throw normalizeBackendError(error, { code: "BLOCKCHAIN_CALL_FAILED", message: "Unable to settle commitment on chain.", status: 502, diff --git a/verify-project-temp.ipynb b/verify-project-temp.ipynb new file mode 100644 index 00000000..26bb0d4a --- /dev/null +++ b/verify-project-temp.ipynb @@ -0,0 +1,1175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ae942b7e", + "metadata": {}, + "source": [ + "# Verify Commitlabs Frontend Setup\n", + "This notebook runs the key validation steps for dependencies, tests, linting, and the production build." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6117c385", + "metadata": {}, + "outputs": [], + "source": [ + "1 + 1\n" + ] + }, + { + "cell_type": "markdown", + "id": "2b43a98a", + "metadata": {}, + "source": [ + "## 1. Install Dependencies\n", + "Use the workspace package manager to install all dependencies and verify the lockfile is consistent." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c29aa592", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Python executable: /usr/local/python/3.12.1/bin/python\n", + "pnpm path: /home/codespace/nvm/current/bin/pnpm\n", + "Node version: v24.14.0\n" + ] + } + ], + "source": [ + "import subprocess\n", + "import sys\n", + "print('Python executable:', sys.executable)\n", + "print('pnpm path:', subprocess.run(['which','pnpm'], capture_output=True, text=True).stdout.strip())\n", + "print('Node version:', subprocess.run(['node','--version'], capture_output=True, text=True).stdout.strip())\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4a9a55e1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lockfile is up to date, resolution step is skipped\n", + "Progress: resolved \u001b[96m1\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m0\u001b[39m, added \u001b[96m0\u001b[39m\n", + "\u001b[1APackages: \u001b[32m+506\u001b[39m\u001b[0K\n", + "\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\u001b[32m+\u001b[39m\n", + "Progress: resolved \u001b[96m1\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m0\u001b[39m, added \u001b[96m0\u001b[39m\n", + "\u001b[1A\u001b[33m\u001b[39m\u001b[0K\n", + "\u001b[33m ╭─────────────────────────────────────────╮\u001b[39m\n", + " \u001b[33m│\u001b[39m \u001b[33m│\u001b[39m\n", + " \u001b[33m│\u001b[39m Update available! \u001b[31m10.32.1\u001b[39m → \u001b[32m11.4.0\u001b[39m. \u001b[33m│\u001b[39m\n", + " \u001b[33m│\u001b[39m \u001b[35mChangelog:\u001b[39m https://pnpm.io/v/11.4.0 \u001b[33m│\u001b[39m\n", + " \u001b[33m│\u001b[39m To update, run: \u001b[35mpnpm add -g pnpm\u001b[39m \u001b[33m│\u001b[39m\n", + " \u001b[33m│\u001b[39m \u001b[33m│\u001b[39m\n", + "\u001b[33m ╰─────────────────────────────────────────╯\u001b[39m\n", + "\u001b[33m\u001b[39m\n", + "Progress: resolved \u001b[96m1\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m0\u001b[39m, added \u001b[96m0\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m0\u001b[39m, added \u001b[96m0\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m12\u001b[39m, added \u001b[96m0\u001b[39m\n", + "Downloading sodium-native@4.3.3: \u001b[96m0.00 B\u001b[39m/\u001b[96m5.76 MB\u001b[39m\n", + "\u001b[1ADownloading sodium-native@4.3.3: \u001b[96m3.29 kB\u001b[39m/\u001b[96m5.76 MB\u001b[39m\n", + "\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m28\u001b[39m, added \u001b[96m4\u001b[39m\n", + "\u001b[1B\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m43\u001b[39m, added \u001b[96m8\u001b[39m\n", + "\u001b[1B\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m74\u001b[39m, added \u001b[96m25\u001b[39m\n", + "\u001b[1B\u001b[2ADownloading sodium-native@4.3.3: \u001b[96m5.76 MB\u001b[39m/\u001b[96m5.76 MB\u001b[39m, done\u001b[0K\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m74\u001b[39m, added \u001b[96m25\u001b[39m\n", + "Downloading next@14.2.35: \u001b[96m0.00 B\u001b[39m/\u001b[96m20.82 MB\u001b[39m\n", + "\u001b[1ADownloading next@14.2.35: \u001b[96m7.52 kB\u001b[39m/\u001b[96m20.82 MB\u001b[39m\n", + "\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m82\u001b[39m, added \u001b[96m25\u001b[39m\n", + "\u001b[1BDownloading @next/swc-linux-x64-gnu@14.2.33: \u001b[96m0.00 B\u001b[39m/\u001b[96m41.91 MB\u001b[39m\n", + "\u001b[1ADownloading @next/swc-linux-x64-gnu@14.2.33: \u001b[96m11.49 kB\u001b[39m/\u001b[96m41.91 MB\u001b[39m\n", + "\u001b[3AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m89\u001b[39m, added \u001b[96m29\u001b[39m\n", + "\u001b[2B\u001b[3AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m93\u001b[39m, added \u001b[96m29\u001b[39m\n", + "\u001b[2BDownloading @next/swc-linux-x64-musl@14.2.33: \u001b[96m0.00 B\u001b[39m/\u001b[96m50.27 MB\u001b[39m\n", + "\u001b[1ADownloading @next/swc-linux-x64-musl@14.2.33: \u001b[96m15.58 kB\u001b[39m/\u001b[96m50.27 MB\u001b[39m\n", + "\u001b[4ADownloading next@14.2.35: \u001b[96m20.82 MB\u001b[39m/\u001b[96m20.82 MB\u001b[39m, done\u001b[0K\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m93\u001b[39m, added \u001b[96m29\u001b[39m\n", + "\u001b[2B\u001b[3ADownloading @next/swc-linux-x64-gnu@14.2.33: \u001b[96m41.91 MB\u001b[39m/\u001b[96m41.91 MB\u001b[39m, done\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m93\u001b[39m, added \u001b[96m29\u001b[39m\u001b[0K\n", + "\u001b[1B\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m94\u001b[39m, added \u001b[96m29\u001b[39m\n", + "\u001b[1B\u001b[2ADownloading @next/swc-linux-x64-musl@14.2.33: \u001b[96m50.27 MB\u001b[39m/\u001b[96m50.27 MB\u001b[39m, done\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m94\u001b[39m, added \u001b[96m29\u001b[39m\u001b[0K\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m97\u001b[39m, added \u001b[96m31\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m98\u001b[39m, added \u001b[96m31\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m105\u001b[39m, added \u001b[96m33\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m107\u001b[39m, added \u001b[96m33\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m108\u001b[39m, added \u001b[96m33\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m109\u001b[39m, added \u001b[96m33\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m134\u001b[39m, added \u001b[96m45\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m150\u001b[39m, added \u001b[96m50\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m171\u001b[39m, added \u001b[96m57\u001b[39m\n", + "Downloading @rolldown/binding-linux-x64-gnu@1.0.0: \u001b[96m0.00 B\u001b[39m/\u001b[96m8.65 MB\u001b[39m\n", + "\u001b[1ADownloading @rolldown/binding-linux-x64-gnu@1.0.0: \u001b[96m3.58 kB\u001b[39m/\u001b[96m8.65 MB\u001b[39m\n", + "\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m200\u001b[39m, added \u001b[96m69\u001b[39m\n", + "\u001b[1BDownloading @rolldown/binding-linux-x64-musl@1.0.0: \u001b[96m0.00 B\u001b[39m/\u001b[96m8.61 MB\u001b[39m\n", + "\u001b[1ADownloading @rolldown/binding-linux-x64-musl@1.0.0: \u001b[96m15.74 kB\u001b[39m/\u001b[96m8.61 MB\u001b[39m\n", + "\u001b[3AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m209\u001b[39m, added \u001b[96m73\u001b[39m\n", + "\u001b[2B\u001b[3AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m220\u001b[39m, added \u001b[96m77\u001b[39m\n", + "\u001b[2B\u001b[3ADownloading @rolldown/binding-linux-x64-gnu@1.0.0: \u001b[96m8.65 MB\u001b[39m/\u001b[96m8.65 MB\u001b[39m, done\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m220\u001b[39m, added \u001b[96m77\u001b[39m\u001b[0K\n", + "\u001b[1B\u001b[2ADownloading @rolldown/binding-linux-x64-musl@1.0.0: \u001b[96m8.61 MB\u001b[39m/\u001b[96m8.61 MB\u001b[39m, done\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m220\u001b[39m, added \u001b[96m77\u001b[39m\u001b[0K\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m231\u001b[39m, added \u001b[96m81\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m232\u001b[39m, added \u001b[96m81\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m268\u001b[39m, added \u001b[96m93\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m285\u001b[39m, added \u001b[96m97\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m302\u001b[39m, added \u001b[96m104\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m342\u001b[39m, added \u001b[96m124\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m347\u001b[39m, added \u001b[96m124\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m358\u001b[39m, added \u001b[96m128\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m361\u001b[39m, added \u001b[96m128\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m370\u001b[39m, added \u001b[96m132\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m381\u001b[39m, added \u001b[96m135\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m426\u001b[39m, added \u001b[96m154\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m449\u001b[39m, added \u001b[96m162\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m461\u001b[39m, added \u001b[96m166\u001b[39m\n", + "Downloading react-icons@5.6.0: \u001b[96m0.00 B\u001b[39m/\u001b[96m22.42 MB\u001b[39m\n", + "\u001b[1ADownloading react-icons@5.6.0: \u001b[96m15.74 kB\u001b[39m/\u001b[96m22.42 MB\u001b[39m\n", + "\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m473\u001b[39m, added \u001b[96m170\u001b[39m\n", + "\u001b[1B\u001b[2AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m481\u001b[39m, added \u001b[96m173\u001b[39m\n", + "\u001b[1B\u001b[2ADownloading react-icons@5.6.0: \u001b[96m22.42 MB\u001b[39m/\u001b[96m22.42 MB\u001b[39m, done\u001b[0K\n", + "Progress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m481\u001b[39m, added \u001b[96m173\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m482\u001b[39m, added \u001b[96m173\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m488\u001b[39m, added \u001b[96m174\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m489\u001b[39m, added \u001b[96m174\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m506\u001b[39m, added \u001b[96m231\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m506\u001b[39m, added \u001b[96m370\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m506\u001b[39m, added \u001b[96m434\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m506\u001b[39m, added \u001b[96m489\u001b[39m\n", + "\u001b[1AProgress: resolved \u001b[96m506\u001b[39m, reused \u001b[96m0\u001b[39m, downloaded \u001b[96m506\u001b[39m, added \u001b[96m506\u001b[39m, done\n", + "\n", + "\u001b[96mdependencies:\u001b[39m\n", + "\u001b[32m+\u001b[39m @stellar/freighter-api \u001b[90m1.7.1\u001b[39m\n", + "\u001b[32m+\u001b[39m @stellar/stellar-sdk \u001b[90m11.3.0\u001b[39m\n", + "\u001b[32m+\u001b[39m @tailwindcss/postcss \u001b[90m4.3.0\u001b[39m\n", + "\u001b[32m+\u001b[39m clsx \u001b[90m2.1.1\u001b[39m\n", + "\u001b[32m+\u001b[39m framer-motion \u001b[90m12.38.0\u001b[39m\n", + "\u001b[32m+\u001b[39m lucide-react \u001b[90m0.563.0\u001b[39m\n", + "\u001b[32m+\u001b[39m next \u001b[90m14.2.35\u001b[39m\n", + "\u001b[32m+\u001b[39m postcss \u001b[90m8.5.14\u001b[39m\n", + "\u001b[32m+\u001b[39m react \u001b[90m18.3.1\u001b[39m\n", + "\u001b[32m+\u001b[39m react-dom \u001b[90m18.3.1\u001b[39m\n", + "\u001b[32m+\u001b[39m react-icons \u001b[90m5.6.0\u001b[39m\n", + "\u001b[32m+\u001b[39m recharts \u001b[90m3.8.1\u001b[39m\n", + "\u001b[32m+\u001b[39m tailwind-merge \u001b[90m3.6.0\u001b[39m\n", + "\u001b[32m+\u001b[39m tailwindcss \u001b[90m4.3.0\u001b[39m\n", + "\u001b[32m+\u001b[39m zod \u001b[90m4.4.3\u001b[39m\n", + "\n", + "\u001b[96mdevDependencies:\u001b[39m\n", + "\u001b[32m+\u001b[39m @testing-library/jest-dom \u001b[90m6.9.1\u001b[39m\n", + "\u001b[32m+\u001b[39m @testing-library/react \u001b[90m16.3.2\u001b[39m\n", + "\u001b[32m+\u001b[39m @types/node \u001b[90m20.19.41\u001b[39m\n", + "\u001b[32m+\u001b[39m @types/react \u001b[90m18.3.28\u001b[39m\n", + "\u001b[32m+\u001b[39m @types/react-dom \u001b[90m18.3.7\u001b[39m\n", + "\u001b[32m+\u001b[39m @vitest/coverage-v8 \u001b[90m4.1.6\u001b[39m\n", + "\u001b[32m+\u001b[39m @vitest/ui \u001b[90m4.1.6\u001b[39m\n", + "\u001b[32m+\u001b[39m eslint \u001b[90m8.57.1\u001b[39m\n", + "\u001b[32m+\u001b[39m eslint-config-next \u001b[90m14.2.35\u001b[39m\n", + "\u001b[32m+\u001b[39m happy-dom \u001b[90m20.9.0\u001b[39m\n", + "\u001b[32m+\u001b[39m node-fetch \u001b[90m3.3.2\u001b[39m\n", + "\u001b[32m+\u001b[39m tsx \u001b[90m4.21.0\u001b[39m\n", + "\u001b[32m+\u001b[39m typescript \u001b[90m5.9.3\u001b[39m\n", + "\u001b[32m+\u001b[39m vitest \u001b[90m4.1.6\u001b[39m\n", + "\n", + "\u001b[33m╭ Warning ─────────────────────────────────────────────────────────────────────╮\u001b[39m\n", + "\u001b[33m│\u001b[39m \u001b[33m│\u001b[39m\n", + "\u001b[33m│\u001b[39m Ignored build scripts: esbuild@0.27.7, unrs-resolver@1.11.1. \u001b[33m│\u001b[39m\n", + "\u001b[33m│\u001b[39m Run \"pnpm approve-builds\" to pick which dependencies should be allowed \u001b[33m│\u001b[39m\n", + "\u001b[33m│\u001b[39m to run scripts. \u001b[33m│\u001b[39m\n", + "\u001b[33m│\u001b[39m \u001b[33m│\u001b[39m\n", + "\u001b[33m╰──────────────────────────────────────────────────────────────────────────────╯\u001b[39m\n", + "Done in 16.8s using pnpm v10.32.1\n" + ] + } + ], + "source": [ + "!cd /workspaces/Commitlabs-Frontend && pnpm install --frozen-lockfile" + ] + }, + { + "cell_type": "markdown", + "id": "798be1d7", + "metadata": {}, + "source": [ + "## 2. Run Unit Tests\n", + "Execute the project's unit tests with Vitest and capture the results." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7835e56", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "> commitlabs-frontend@0.1.0 test /workspaces/Commitlabs-Frontend\n", + "> vitest -- --run\n", + "\n", + "\u001b[?25l\n", + "\u001b[1m\u001b[30m\u001b[44m DEV \u001b[49m\u001b[39m\u001b[22m \u001b[34mv4.1.6 \u001b[39m\u001b[90m/workspaces/Commitlabs-Frontend\u001b[39m\n", + "\n", + "\u001b[?2026h\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/env.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m0 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m0 passed\u001b[39m\u001b[22m\u001b[90m (0)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m293ms\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/env.test.ts\u001b[2m 0/51\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m0 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m0 passed\u001b[39m\u001b[22m\u001b[90m (51)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m793ms\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/env.test.ts \u001b[2m(\u001b[22m\u001b[2m51 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 23\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/env.test.ts\u001b[2m 51/51\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m1 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m51 passed\u001b[39m\u001b[22m\u001b[90m (51)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m995ms\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/user-preferences.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m1 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m51 passed\u001b[39m\u001b[22m\u001b[90m (51)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.09s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/user-preferences.test.ts\u001b[2m 0/26\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m1 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m51 passed\u001b[39m\u001b[22m\u001b[90m (77)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.40s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when Authorization header is absent\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.705Z\",\"requestId\":\"bd920339-73fd-411e-8704-d392daf94604\",\"context\":{\"correlationId\":\"b74fa1b720158a9d8f1108b653dcc0ca\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authorization header is required.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when Authorization header has wrong scheme\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.711Z\",\"requestId\":\"898c4d6c-5595-4bb4-be79-ba6880d146fe\",\"context\":{\"correlationId\":\"76d9ab0e93542500f47c7382b82fb700\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authorization header must be in format: Bearer \",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when Bearer token has wrong format\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.712Z\",\"requestId\":\"5dd90536-cf57-4da6-b006-9960946bb868\",\"context\":{\"correlationId\":\"aa5640c418871f3408a58db6e4c8e59e\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Invalid or expired session token.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when Authorization header is empty string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.713Z\",\"requestId\":\"e993a448-cc25-4bae-a7d5-474018b66f36\",\"context\":{\"correlationId\":\"2f09844bffb801384706ec75f0362456\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authorization header is required.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when token has no address segment\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.715Z\",\"requestId\":\"1e90b845-025a-4968-b64a-b967599f7206\",\"context\":{\"correlationId\":\"501f897ef4afb71a994eea973ec97936\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Invalid or expired session token.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when Authorization header is absent\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.723Z\",\"requestId\":\"67733220-2ea3-48ff-a784-e6a80d5d5f5e\",\"context\":{\"correlationId\":\"bd82e71aa6c645c9579f9e1715e410fd\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authorization header is required.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 401 when token format is invalid\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.724Z\",\"requestId\":\"f89d08bf-3e04-48c3-a05e-f108e4ee68af\",\"context\":{\"correlationId\":\"d902a90d7315fb88d19f48a954ecfffc\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Invalid or expired session token.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 400 for unsupported displayCurrency\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.728Z\",\"requestId\":\"d7ef9bd5-7005-43fa-97d4-6758e6479249\",\"context\":{\"correlationId\":\"43ba4e75d3446c81973abef7ce24d6db\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid preference data.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 400 for invalid theme value\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.732Z\",\"requestId\":\"0417c7cc-ffd9-4a52-bee9-4210fd508dc4\",\"context\":{\"correlationId\":\"d888216b57234c2ec43ac46a0004a743\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid preference data.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 400 for invalid language tag\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.736Z\",\"requestId\":\"ba5a53b4-ecc3-403d-b460-ad34ac627d71\",\"context\":{\"correlationId\":\"7ab3c5d4d3a82a790ecf907886b3faad\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid preference data.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 400 when notifications.email is not boolean\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.738Z\",\"requestId\":\"6fda1304-9baf-4d8c-8d64-6f56a10078c7\",\"context\":{\"correlationId\":\"58ce9e960c8c77e8bad7fee76f0b293c\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid preference data.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 400 for an empty payload\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.740Z\",\"requestId\":\"c7659081-fc9e-452d-823e-9614d9d2bd2d\",\"context\":{\"correlationId\":\"6a528b4dd2a49af6b2801bd372dce84b\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Request body must contain at least one preference field.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/user-preferences.test.ts\u001b[2m > \u001b[22m\u001b[2mPUT /api/user/preferences\u001b[2m > \u001b[22m\u001b[2mreturns 400 for non-JSON body\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:19.741Z\",\"requestId\":\"3e0e1d7b-e639-4ab1-8e89-54ffd4ffcce8\",\"context\":{\"correlationId\":\"57ee3d2ce913d927cca693827910b4fb\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Request body must be valid JSON.\",\"url\":\"http://localhost:3000/api/user/preferences\",\"method\":\"PUT\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/attestations/recent/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m2 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m77 passed\u001b[39m\u001b[22m\u001b[90m (77)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.60s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/user-preferences.test.ts \u001b[2m(\u001b[22m\u001b[2m26 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 63\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/attestations/recent/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m2 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m77 passed\u001b[39m\u001b[22m\u001b[90m (77)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.60s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/attestations/recent/route.test.ts\u001b[2m 0/25\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m2 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m77 passed\u001b[39m\u001b[22m\u001b[90m (102)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.70s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — ownerAddress filter\u001b[2m > \u001b[22m\u001b[2mreturns 401 when ownerAddress is provided without auth token\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.065Z\",\"requestId\":\"adaa0d98-92f4-4f71-b9eb-bf563f010f70\",\"context\":{\"correlationId\":\"761d122f5e2ba6ae8a0a8fcfc3230fc2\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authentication is required to filter attestations by ownerAddress.\",\"url\":\"http://localhost:3000/api/attestations/recent?ownerAddress=GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — ownerAddress filter\u001b[2m > \u001b[22m\u001b[2mreturns 401 when Authorization header is malformed (no Bearer prefix)\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.067Z\",\"requestId\":\"d38ba2a8-6f68-4dc3-b5fc-4629218bc7ea\",\"context\":{\"correlationId\":\"72d33740820fa5d203f4523603e77f48\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authentication is required to filter attestations by ownerAddress.\",\"url\":\"http://localhost:3000/api/attestations/recent?ownerAddress=GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is 0 (below minimum)\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.068Z\",\"requestId\":\"4e464bf3-5e2a-4c05-9396-53a654ba139f\",\"context\":{\"correlationId\":\"822db72489a8854f6d491a493b6daa77\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be between 1 and 100. Received: 0.\",\"url\":\"http://localhost:3000/api/attestations/recent?limit=0\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is negative\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.069Z\",\"requestId\":\"bcd94903-b005-4e9e-909c-39a08ffbd1a0\",\"context\":{\"correlationId\":\"ddc69669859b62d605a27a5257d582b6\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be between 1 and 100. Received: -5.\",\"url\":\"http://localhost:3000/api/attestations/recent?limit=-5\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit exceeds 100\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.070Z\",\"requestId\":\"30ebcdf3-cb8f-4d0d-9e40-ec0f48fb0bf5\",\"context\":{\"correlationId\":\"bd962ad4948b373fa483fd61de7ca154\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be between 1 and 100. Received: 101.\",\"url\":\"http://localhost:3000/api/attestations/recent?limit=101\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is a non-numeric string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.070Z\",\"requestId\":\"3a8209a8-5459-439c-97ea-769af37654c4\",\"context\":{\"correlationId\":\"55d1c32ab159b66bc7a9ada7dd08434e\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be an integer between 1 and 100.\",\"url\":\"http://localhost:3000/api/attestations/recent?limit=abc\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is a float\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.071Z\",\"requestId\":\"d774aa65-16a2-4231-8570-ab6e702e224c\",\"context\":{\"correlationId\":\"2de73586e0bc79f73cb814f2ee4d9a63\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be an integer between 1 and 100.\",\"url\":\"http://localhost:3000/api/attestations/recent?limit=2.5\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when ownerAddress is an empty string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.072Z\",\"requestId\":\"5b2523c5-a616-4d5b-9b1a-6bb8fb560cf4\",\"context\":{\"correlationId\":\"6d90ab20cfe00c0fe1b42d510de82d16\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"ownerAddress\\\" must be a non-empty string when provided.\",\"url\":\"http://localhost:3000/api/attestations/recent?ownerAddress=+++\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/attestations/recent/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/attestations/recent — rate limiting\u001b[2m > \u001b[22m\u001b[2mreturns 429 when rate limit is exceeded\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:20.073Z\",\"requestId\":\"4b85bf77-3f05-45a7-9a21-078de7ab2430\",\"context\":{\"correlationId\":\"80a64c265ed9820e98155017eefcc7ac\",\"code\":\"TOO_MANY_REQUESTS\",\"status\":429,\"message\":\"Too many requests. Please try again later.\",\"url\":\"http://localhost:3000/api/attestations/recent\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/lib/backend/errorCodes.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m3 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m102 passed\u001b[39m\u001b[22m\u001b[90m (102)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.90s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/attestations/recent/route.test.ts \u001b[2m(\u001b[22m\u001b[2m25 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 40\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/lib/backend/errorCodes.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m3 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m102 passed\u001b[39m\u001b[22m\u001b[90m (102)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m1.90s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/lib/backend/errorCodes.test.ts \u001b[2m(\u001b[22m\u001b[2m40 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 24\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/contracts.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m4 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m142 passed\u001b[39m\u001b[22m\u001b[90m (142)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.10s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/contracts.test.ts\u001b[2m 1/51\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m4 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m143 passed\u001b[39m\u001b[22m\u001b[90m (193)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.30s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/contracts.test.ts \u001b[2m(\u001b[22m\u001b[2m51 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 27\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/protocol-constants.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m5 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m193 passed\u001b[39m\u001b[22m\u001b[90m (193)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.40s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — response shape\u001b[2m > \u001b[22m\u001b[2mshould return HTTP 200 with success envelope\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.800Z\",\"requestId\":\"d0a49187-deb6-4b5d-bf77-fecf8370a222\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — response shape\u001b[2m > \u001b[22m\u001b[2mshould include all top-level keys in the constants payload\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.807Z\",\"requestId\":\"e798c170-f5c7-430d-ac0b-caa6bc124318\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — response shape\u001b[2m > \u001b[22m\u001b[2mshould return fees with expected fields and types\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.808Z\",\"requestId\":\"5a8b4ae9-1daf-4705-a4d2-112b539e1c85\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — response shape\u001b[2m > \u001b[22m\u001b[2mshould return penalties as a non-empty array with correct shape\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.809Z\",\"requestId\":\"2398ef34-c9b4-48f9-8774-4b3ed2cfd14c\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — response shape\u001b[2m > \u001b[22m\u001b[2mshould return commitment limits with correct fields and types\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.811Z\",\"requestId\":\"5343194e-1a65-4485-a21d-24090e08d8dd\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — response shape\u001b[2m > \u001b[22m\u001b[2mshould return a valid ISO-8601 cachedAt timestamp\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.812Z\",\"requestId\":\"1a47e146-1f3a-4156-b210-b058b19cdd5e\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — default values\u001b[2m > \u001b[22m\u001b[2mshould use default penalty percentages (2/3/5) when env is unset\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.814Z\",\"requestId\":\"4cf539ad-bd27-4f6a-8750-b225913ead14\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — default values\u001b[2m > \u001b[22m\u001b[2mshould default protocolVersion to 'v1'\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.815Z\",\"requestId\":\"51256dee-5b2b-48a2-a795-31a8126350bd\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — default values\u001b[2m > \u001b[22m\u001b[2mshould default network base fee to 100 stroops\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.816Z\",\"requestId\":\"e38b0f39-a8e1-45d2-aba7-97d0ae4c7cb7\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — default values\u001b[2m > \u001b[22m\u001b[2mshould default platform fee to 0%\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.817Z\",\"requestId\":\"1a7bf6b5-0b2b-43d5-9bfb-a8ff352c6c7e\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — env overrides\u001b[2m > \u001b[22m\u001b[2mshould respect COMMITLABS_PLATFORM_FEE_PERCENT override\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.818Z\",\"requestId\":\"f2947d79-5e6e-4460-b6ab-e4dd9dbd9c9a\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — env overrides\u001b[2m > \u001b[22m\u001b[2mshould respect COMMITLABS_MIN_AMOUNT_XLM override\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.819Z\",\"requestId\":\"2c70493a-05bf-459b-b91c-7107360d3650\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — env overrides\u001b[2m > \u001b[22m\u001b[2mshould respect COMMITLABS_PENALTY_TIERS_JSON override\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.820Z\",\"requestId\":\"06e2993b-9cda-4239-a658-aa9922372f0b\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — caching\u001b[2m > \u001b[22m\u001b[2mshould set Cache-Control header for browser/CDN caching\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.821Z\",\"requestId\":\"cc7aaeac-63c7-4467-a3dd-09db1ad7fd96\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — security headers\u001b[2m > \u001b[22m\u001b[2mshould include X-Content-Type-Options nosniff header\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.827Z\",\"requestId\":\"bfa17200-46e3-4553-8fb9-44461b3391cf\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — security headers\u001b[2m > \u001b[22m\u001b[2mshould include X-Frame-Options DENY header\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.827Z\",\"requestId\":\"5ed1e469-0c43-4bdd-ba4e-604ffa6bd660\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/protocol-constants.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/protocol/constants — security headers\u001b[2m > \u001b[22m\u001b[2mshould include Content-Security-Policy header\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Protocol constants requested\",\"timestamp\":\"2026-05-28T12:35:20.828Z\",\"requestId\":\"99c73b72-5b32-475c-ae86-ce5e3e95cbd7\"}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/protocol-constants.test.ts\u001b[2m 1/27\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m5 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m194 passed\u001b[39m\u001b[22m\u001b[90m (220)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.50s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/protocol-constants.test.ts \u001b[2m(\u001b[22m\u001b[2m27 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 37\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/admin/audit-events/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m6 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m220 passed\u001b[39m\u001b[22m\u001b[90m (220)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.70s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/admin/audit-events/route.test.ts\u001b[2m 0/21\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m6 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m220 passed\u001b[39m\u001b[22m\u001b[90m (241)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.81s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — feature disabled\u001b[2m > \u001b[22m\u001b[2mreturns 403 when feature flag is disabled\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.124Z\",\"requestId\":\"8a64aae6-db5d-4b69-8932-b4af967f3cb5\",\"context\":{\"correlationId\":\"27ce113def893f8b2ff4e9e5dc0eb364\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Audit log feature is disabled.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — feature disabled\u001b[2m > \u001b[22m\u001b[2mreturns 403 even with valid admin token when disabled\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.136Z\",\"requestId\":\"9793bf27-d745-413d-abf9-26b2c67f09d8\",\"context\":{\"correlationId\":\"7cd3842642347ac27eda20d80dcce787\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Audit log feature is disabled.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — unauthorized\u001b[2m > \u001b[22m\u001b[2mreturns 403 when Authorization header is missing\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.138Z\",\"requestId\":\"a5cd02aa-0ca1-4ffe-b91e-fff4bc592215\",\"context\":{\"correlationId\":\"a1b95a194e799a6c13434345ce74de9e\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Invalid or missing admin token.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — unauthorized\u001b[2m > \u001b[22m\u001b[2mreturns 403 when Authorization header is malformed (no Bearer prefix)\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.140Z\",\"requestId\":\"c1cca338-9661-4a4e-a2dd-ddd75d6586aa\",\"context\":{\"correlationId\":\"adcba9f3a4e25d74d24cba261dba4eab\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Invalid or missing admin token.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — unauthorized\u001b[2m > \u001b[22m\u001b[2mreturns 403 when admin token is incorrect\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.144Z\",\"requestId\":\"59cc48de-c346-4e3e-9293-e6e85711542a\",\"context\":{\"correlationId\":\"9ad5a1ef15e693b6199771fc763c02ed\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Invalid or missing admin token.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — unauthorized\u001b[2m > \u001b[22m\u001b[2mreturns 403 when admin token is an empty string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.146Z\",\"requestId\":\"47cb84d3-2152-408b-8cfa-159f9d4931a4\",\"context\":{\"correlationId\":\"9c63ae920ad605ef172e2d13ec631716\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Invalid or missing admin token.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — unauthorized\u001b[2m > \u001b[22m\u001b[2mreturns 403 when COMMITLABS_ADMIN_SECRET is not configured\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.147Z\",\"requestId\":\"d172a68d-559d-4f37-90fd-c370bf8f176e\",\"context\":{\"correlationId\":\"d1d4a4eb07cf4c6b3491d64bccee94ec\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Admin access is not configured.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is 0 (below minimum)\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.162Z\",\"requestId\":\"f71c8b28-260e-4729-895b-7f11839a9901\",\"context\":{\"correlationId\":\"9d7bfcf7771943a6282f0a5bcd832d44\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be between 1 and 200. Received: 0.\",\"url\":\"http://localhost:3000/api/admin/audit-events?limit=0\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is negative\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.163Z\",\"requestId\":\"9b900cf5-ecfd-476a-af71-22feba8f4142\",\"context\":{\"correlationId\":\"d4aff5d6be4ce59e903d7c12347ec390\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be between 1 and 200. Received: -5.\",\"url\":\"http://localhost:3000/api/admin/audit-events?limit=-5\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit exceeds 200\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.165Z\",\"requestId\":\"6287d231-92ec-4fd7-a427-e74e68932fcf\",\"context\":{\"correlationId\":\"812074d4e3b6256d7c423330f1c95652\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be between 1 and 200. Received: 201.\",\"url\":\"http://localhost:3000/api/admin/audit-events?limit=201\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is a non-numeric string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.165Z\",\"requestId\":\"99f30d65-42c2-4403-91fc-d8492c62720e\",\"context\":{\"correlationId\":\"f451cd1d52a2ebf8c7fadee616613661\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be an integer between 1 and 200.\",\"url\":\"http://localhost:3000/api/admin/audit-events?limit=abc\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — limit validation\u001b[2m > \u001b[22m\u001b[2mreturns 400 when limit is a float\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.166Z\",\"requestId\":\"44b7b272-dfb6-4470-a186-1ad24f7ba330\",\"context\":{\"correlationId\":\"db8586293dead60604bb1b249dac1946\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"\\\"limit\\\" must be an integer between 1 and 200.\",\"url\":\"http://localhost:3000/api/admin/audit-events?limit=2.5\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/admin/audit-events/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/admin/audit-events — rate limiting\u001b[2m > \u001b[22m\u001b[2mreturns 429 when rate limit is exceeded\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.167Z\",\"requestId\":\"b3cb017e-1112-487d-8550-33a9994f5e22\",\"context\":{\"correlationId\":\"8a4e6947cf436d8a43702231465cc190\",\"code\":\"TOO_MANY_REQUESTS\",\"status\":429,\"message\":\"Too many requests. Please try again later.\",\"url\":\"http://localhost:3000/api/admin/audit-events\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/admin/audit-events/route.test.ts\u001b[2m 21/21\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m7 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m241 passed\u001b[39m\u001b[22m\u001b[90m (241)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.90s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/admin/audit-events/route.test.ts \u001b[2m(\u001b[22m\u001b[2m21 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 55\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/admin/audit-events/route.test.ts\u001b[2m 21/21\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m7 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m241 passed\u001b[39m\u001b[22m\u001b[90m (241)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m2.90s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/parsing.test.ts \u001b[2m(\u001b[22m\u001b[2m59 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 13\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/cors.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m8 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m300 passed\u001b[39m\u001b[22m\u001b[90m (300)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m3.21s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/cors.test.ts \u001b[2m(\u001b[22m\u001b[2m15 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 20\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitmentHistory.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m9 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m315 passed\u001b[39m\u001b[22m\u001b[90m (315)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m3.41s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/commitmentHistory.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/[id]/history\u001b[2m > \u001b[22m\u001b[2mreturns 404 when commitment does not exist\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:21.817Z\",\"requestId\":\"250a18a7-c99b-4f47-9c56-05b4ffa19cbe\",\"context\":{\"correlationId\":\"674ec110436c82802bfd88624d7caf54\",\"code\":\"NOT_FOUND\",\"status\":404,\"message\":\"Commitment not found.\",\"url\":\"http://localhost:3000/api/commitments/CMT-MISSING/history\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitmentHistory.test.ts\u001b[2m 1/17\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m9 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m316 passed\u001b[39m\u001b[22m\u001b[90m (332)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m3.61s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/commitmentHistory.test.ts \u001b[2m(\u001b[22m\u001b[2m17 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 133\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/attestationSchemas.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m10 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m332 passed\u001b[39m\u001b[22m\u001b[90m (332)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m3.71s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/attestationSchemas.test.ts\u001b[2m 1/44\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m10 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m333 passed\u001b[39m\u001b[22m\u001b[90m (376)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m3.81s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/attestationSchemas.test.ts \u001b[2m(\u001b[22m\u001b[2m44 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 20\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/attestationSchemas.test.ts\u001b[2m 44/44\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m11 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m376 passed\u001b[39m\u001b[22m\u001b[90m (376)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m3.91s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/seed.test.ts\u001b[2m 0/22\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m11 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m376 passed\u001b[39m\u001b[22m\u001b[90m (398)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m4.01s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/seed.test.ts \u001b[2m(\u001b[22m\u001b[2m22 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 83\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/__tests__/apiResponse.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m12 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m398 passed\u001b[39m\u001b[22m\u001b[90m (398)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m4.31s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/__tests__/apiResponse.test.ts \u001b[2m(\u001b[22m\u001b[2m16 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 18\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitments-write-rate-limit.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m13 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m414 passed\u001b[39m\u001b[22m\u001b[90m (414)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m4.51s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitments-write-rate-limit.test.ts\u001b[2m 0/9\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m13 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m414 passed\u001b[39m\u001b[22m\u001b[90m (423)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m4.91s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitments-write-rate-limit.test.ts\u001b[2m 1/9\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m13 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m415 passed\u001b[39m\u001b[22m\u001b[90m (423)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.21s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/commitments-write-rate-limit.test.ts \u001b[2m(\u001b[22m\u001b[2m9 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 621\u001b[2mms\u001b[22m\u001b[39m\n", + " \u001b[33m\u001b[2m✓\u001b[22m\u001b[39m returns 429 with Retry-After when rate limit is exceeded \u001b[33m 598\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/cache/adapter.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m14 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m423 passed\u001b[39m\u001b[22m\u001b[90m (423)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.31s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/cache/adapter.test.ts \u001b[2m(\u001b[22m\u001b[2m24 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 64\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/marketplace-purchase.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m15 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m447 passed\u001b[39m\u001b[22m\u001b[90m (447)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.52s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstdout\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 200 with purchase details on success\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Marketplace purchase initiated\",\"timestamp\":\"2026-05-28T12:35:23.924Z\",\"requestId\":\"1c4c5968-079e-4bde-98ca-f71b1fc0a785\",\"context\":{\"listingId\":\"listing_1\",\"buyerAddress\":\"GBUYERADDRESS000000000000000000000000000000000000000000000\"}}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mcalls transferOwnership with correct params\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Marketplace purchase initiated\",\"timestamp\":\"2026-05-28T12:35:23.930Z\",\"requestId\":\"d23700d5-dc0c-4a52-8e88-9dcf441502cc\",\"context\":{\"listingId\":\"listing_1\",\"buyerAddress\":\"GBUYERADDRESS000000000000000000000000000000000000000000000\"}}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mrecords an audit event on success\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Marketplace purchase initiated\",\"timestamp\":\"2026-05-28T12:35:23.932Z\",\"requestId\":\"df1bce0f-9f4f-4452-8631-9f03522c1972\",\"context\":{\"listingId\":\"listing_1\",\"buyerAddress\":\"GBUYERADDRESS000000000000000000000000000000000000000000000\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/marketplace-purchase.test.ts\u001b[2m 1/11\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m15 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m448 passed\u001b[39m\u001b[22m\u001b[90m (458)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.62s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 401 when not authenticated\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:23.935Z\",\"requestId\":\"8c10ee83-7691-4909-9fef-84a735ba0ee8\",\"context\":{\"correlationId\":\"949546d348582499521db8789be64214\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"No session token provided\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 404 when listing does not exist\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:23.937Z\",\"requestId\":\"c00ea254-6f20-4566-b124-7db0694e5e91\",\"context\":{\"correlationId\":\"1c2f1e3da138497e30c3a4bf85f3874e\",\"code\":\"NOT_FOUND\",\"status\":404,\"message\":\"Listing not found.\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 409 when preflight fails (listing inactive)\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:23.938Z\",\"requestId\":\"8bbf3993-d5b1-4ef1-98d6-418b2844d38f\",\"context\":{\"correlationId\":\"bc137e68de88636536a04302a4beccd1\",\"code\":\"CONFLICT\",\"status\":409,\"message\":\"Purchase not eligible: listing_inactive\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 409 when buyer is the seller\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:23.939Z\",\"requestId\":\"fe5e1a69-298c-46f4-9f42-3079136d2595\",\"context\":{\"correlationId\":\"adcc2790586d35c47e0bb89a8377fb57\",\"code\":\"CONFLICT\",\"status\":409,\"message\":\"Purchase not eligible: buyer_is_seller\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mdoes not call transferOwnership when preflight fails\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:23.940Z\",\"requestId\":\"13926f96-2f08-48f0-9ca2-2c47743cbde9\",\"context\":{\"correlationId\":\"a6e1f2b98c0998114d3b6bf32270b59b\",\"code\":\"CONFLICT\",\"status\":409,\"message\":\"Purchase not eligible: listing_inactive\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mdoes not record audit event when preflight fails\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:23.941Z\",\"requestId\":\"81079405-f34b-481d-8e4b-370eeab00e23\",\"context\":{\"correlationId\":\"5bb9ded05b1fb62608a2e03a74ea5c9b\",\"code\":\"CONFLICT\",\"status\":409,\"message\":\"Purchase not eligible: listing_inactive\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/marketplace-purchase.test.ts\u001b[2m 1/11\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m15 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m448 passed\u001b[39m\u001b[22m\u001b[90m (458)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.62s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstdout\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 5xx when on-chain transfer fails\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Marketplace purchase initiated\",\"timestamp\":\"2026-05-28T12:35:23.942Z\",\"requestId\":\"5548c375-3737-477d-ab83-af9e50a7d5a1\",\"context\":{\"listingId\":\"listing_1\",\"buyerAddress\":\"GBUYERADDRESS000000000000000000000000000000000000000000000\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/marketplace-purchase.test.ts\u001b[2m 1/11\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m15 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m448 passed\u001b[39m\u001b[22m\u001b[90m (458)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.62s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mreturns 5xx when on-chain transfer fails\n", + "\u001b[22m\u001b[39m{\"level\":\"error\",\"message\":\"[API] Unhandled exception\",\"timestamp\":\"2026-05-28T12:35:23.943Z\",\"requestId\":\"5548c375-3737-477d-ab83-af9e50a7d5a1\",\"context\":{\"correlationId\":\"3e9ddc1ddd744a62b2f268a86558cc28\",\"url\":\"http://localhost:3000/api/marketplace/listings/listing_1/purchase\",\"method\":\"POST\"},\"error\":{\"name\":\"Error\",\"message\":\"Soroban RPC unreachable\",\"stack\":\"Error: Soroban RPC unreachable\\n at /workspaces/Commitlabs-Frontend/tests/api/marketplace-purchase.test.ts:196:7\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\\n at new Promise ()\\n at runWithCancel (file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\\n at new Promise ()\\n at runWithTimeout (file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/marketplace-purchase.test.ts\u001b[2m 1/11\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m15 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m448 passed\u001b[39m\u001b[22m\u001b[90m (458)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.62s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstdout\u001b[2m | tests/api/marketplace-purchase.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/marketplace/listings/[id]/purchase\u001b[2m > \u001b[22m\u001b[2mincludes txHash in response when transfer returns one\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Marketplace purchase initiated\",\"timestamp\":\"2026-05-28T12:35:23.947Z\",\"requestId\":\"16f2ae4a-5dda-4d31-a79c-2825821f09fc\",\"context\":{\"listingId\":\"listing_1\",\"buyerAddress\":\"GBUYERADDRESS000000000000000000000000000000000000000000000\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/marketplace-purchase.test.ts\u001b[2m 1/11\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m15 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m448 passed\u001b[39m\u001b[22m\u001b[90m (458)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.62s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/marketplace-purchase.test.ts \u001b[2m(\u001b[22m\u001b[2m11 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 29\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitments-export.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m16 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m458 passed\u001b[39m\u001b[22m\u001b[90m (458)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.82s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/commitments-export.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/export\u001b[2m > \u001b[22m\u001b[2mreturns 401 when no Authorization header is provided\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.168Z\",\"requestId\":\"4ff510fd-9d7e-43e9-b731-f0afb3520dd9\",\"context\":{\"correlationId\":\"dc5350564f00216753462d4a48dbb5c3\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authentication required.\",\"url\":\"http://localhost:3000/api/commitments/export?ownerAddress=GABC123OWNERADDRESS\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/commitments-export.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/export\u001b[2m > \u001b[22m\u001b[2mreturns 401 when the session token is invalid\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.174Z\",\"requestId\":\"8ffb6f3d-0c25-449c-95dc-10b6ceb2b7ad\",\"context\":{\"correlationId\":\"0b58c7791c4e7fcda037934a4081fa6b\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Authentication required.\",\"url\":\"http://localhost:3000/api/commitments/export?ownerAddress=GABC123OWNERADDRESS\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/commitments-export.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/export\u001b[2m > \u001b[22m\u001b[2mreturns 400 when ownerAddress query param is missing\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.176Z\",\"requestId\":\"9808cc60-130c-499d-bf47-22295918a86d\",\"context\":{\"correlationId\":\"a7d36c5debb04e6dbbad83ecf1581b62\",\"code\":\"BAD_REQUEST\",\"status\":400,\"message\":\"ownerAddress is required.\",\"url\":\"http://localhost:3000/api/commitments/export\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/commitments-export.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/export\u001b[2m > \u001b[22m\u001b[2mreturns 403 when the authenticated address doesn't match the requested ownerAddress\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.178Z\",\"requestId\":\"090ae325-d207-4cf4-8064-7d8b9fbbbc5b\",\"context\":{\"correlationId\":\"3edbb4d3b80686140ae8456b0c1c2978\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"You do not have permission to perform this action.\",\"url\":\"http://localhost:3000/api/commitments/export?ownerAddress=GDIFFERENTOWNERADDRESS\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/commitments-export.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/export\u001b[2m > \u001b[22m\u001b[2mreturns 429 when rate limiting blocks the request\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.184Z\",\"requestId\":\"01f7be83-5fc8-4890-a9a6-8c14d3d498c4\",\"context\":{\"correlationId\":\"7d3bba14e5c44b48965ce59b16c63a83\",\"code\":\"TOO_MANY_REQUESTS\",\"status\":429,\"message\":\"Too many requests. Please try again later.\",\"url\":\"http://localhost:3000/api/commitments/export?ownerAddress=GABC123OWNERADDRESS\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitments-export.test.ts\u001b[2m 1/10\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m16 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m459 passed\u001b[39m\u001b[22m\u001b[90m (468)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m5.92s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/commitments-export.test.ts \u001b[2m(\u001b[22m\u001b[2m10 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 22\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/commitments/[id]/status/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m17 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m468 passed\u001b[39m\u001b[22m\u001b[90m (468)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.02s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/app/api/commitments/[id]/status/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/[id]/status\u001b[2m > \u001b[22m\u001b[2m404 - not found\u001b[2m > \u001b[22m\u001b[2mreturns 404 when commitment does not exist\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.413Z\",\"requestId\":\"e7e07679-7e10-4737-be58-e6afe5da7532\",\"context\":{\"correlationId\":\"e49fbdec75751926db8888a546d37f22\",\"code\":\"NOT_FOUND\",\"status\":404,\"message\":\"Commitment not found.\",\"url\":\"http://localhost/api/commitments/nonexistent-id/status\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/commitments/[id]/status/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/[id]/status\u001b[2m > \u001b[22m\u001b[2m404 - not found\u001b[2m > \u001b[22m\u001b[2mreturns 404 when commitment id is empty string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.415Z\",\"requestId\":\"75a01863-29b8-4e8b-b229-479dee219ea1\",\"context\":{\"correlationId\":\"8f5c8c5cd3d377b19538b1ab6b596cad\",\"code\":\"NOT_FOUND\",\"status\":404,\"message\":\"Commitment not found.\",\"url\":\"http://localhost/api/commitments//status\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/commitments/[id]/status/route.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/[id]/status\u001b[2m > \u001b[22m\u001b[2m429 - rate limit\u001b[2m > \u001b[22m\u001b[2mreturns 429 when rate limit is exceeded\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:24.416Z\",\"requestId\":\"3e2c9259-7165-4c35-82b4-f55cfce113fa\",\"context\":{\"correlationId\":\"c1c1bd4567789cc291ef5ef955775710\",\"code\":\"TOO_MANY_REQUESTS\",\"status\":429,\"message\":\"Too many requests. Please try again later.\",\"url\":\"http://localhost/api/commitments/commitment-123/status\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/validationErrors.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m18 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m481 passed\u001b[39m\u001b[22m\u001b[90m (481)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.22s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/commitments/[id]/status/route.test.ts \u001b[2m(\u001b[22m\u001b[2m13 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 21\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/validationErrors.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m18 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m481 passed\u001b[39m\u001b[22m\u001b[90m (481)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.22s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/validationErrors.test.ts \u001b[2m(\u001b[22m\u001b[2m17 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 17\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/lib/getClientIp.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m19 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m498 passed\u001b[39m\u001b[22m\u001b[90m (498)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.42s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/lib/getClientIp.test.ts \u001b[2m(\u001b[22m\u001b[2m20 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 15\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/auth/verify/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m20 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m518 passed\u001b[39m\u001b[22m\u001b[90m (518)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.62s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/auth/verify/route.test.ts\u001b[2m 0/12\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m20 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m518 passed\u001b[39m\u001b[22m\u001b[90m (530)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.72s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 429 when rate limited\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.095Z\",\"requestId\":\"5ac5a443-46a1-4b11-99c4-e06d0200bd55\",\"context\":{\"correlationId\":\"7723ca572ef09693d25900617d7bfcc6\",\"code\":\"TOO_MANY_REQUESTS\",\"status\":429,\"message\":\"Rate limit exceeded. Please try again later.\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 400 for invalid JSON body\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.097Z\",\"requestId\":\"140dfcdb-474e-4043-ae3b-6051d985b609\",\"context\":{\"correlationId\":\"ef3b11129a080fd19d9afadfd5028ff8\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid JSON in request body\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 400 when address is missing\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.100Z\",\"requestId\":\"6f2ef1bc-5b5e-41b5-80f4-1ee9b79c8aee\",\"context\":{\"correlationId\":\"8627214e93b2c8c01dc28a5bbea68695\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid request data\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 400 when signature is missing\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.101Z\",\"requestId\":\"93df24ce-b1d0-48bd-a3c5-6360090ec61e\",\"context\":{\"correlationId\":\"67d6b35ef6ea3c8aa122c1d068aa7f48\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid request data\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 400 when message is missing\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.103Z\",\"requestId\":\"a93ec50f-171b-42ff-80b2-c4012570e963\",\"context\":{\"correlationId\":\"994a3055127d53008d89e75c0af2e019\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid request data\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 401 on invalid signature\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.104Z\",\"requestId\":\"9e401f50-5edd-4862-8e6c-54680f936a7b\",\"context\":{\"correlationId\":\"024de89f57c9f77553c8b7291f83732d\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Invalid signature\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 401 on nonce mismatch\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.105Z\",\"requestId\":\"bb7a0757-15bc-4538-8823-3aefb9c28c72\",\"context\":{\"correlationId\":\"6aaa1e6fc22d131f6ed4753780a7752c\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Nonce address mismatch\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 401 when nonce is expired or already consumed\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.107Z\",\"requestId\":\"64db0267-6dfb-470c-bace-fa90fbccc11e\",\"context\":{\"correlationId\":\"af507dbca0985bf2533bd2201ae4f1e7\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Invalid or expired nonce\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 401 with default message when verificationResult has no error string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.109Z\",\"requestId\":\"fc1516d1-b012-485d-a416-cb4a235b9597\",\"context\":{\"correlationId\":\"533848fcf516b98db1bbcef7a6f44111\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"Signature verification failed\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/verify/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/verify\u001b[2m > \u001b[22m\u001b[2mreturns 500 on unexpected handler error\n", + "\u001b[22m\u001b[39m{\"level\":\"error\",\"message\":\"[API] Unhandled exception\",\"timestamp\":\"2026-05-28T12:35:25.111Z\",\"requestId\":\"50cefbc8-aafd-4e27-a274-2c42c906655e\",\"context\":{\"correlationId\":\"c876485e6596bfe862e85d0236a74105\",\"url\":\"http://localhost:3000/api/auth/verify\",\"method\":\"POST\"},\"error\":{\"name\":\"Error\",\"message\":\"boom\",\"stack\":\"Error: boom\\n at /workspaces/Commitlabs-Frontend/src/app/api/auth/verify/route.test.ts:153:74\\n at Mock (file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+spy@4.1.6/node_modules/@vitest/spy/dist/index.js:332:34)\\n at POST.cors (/workspaces/Commitlabs-Frontend/src/app/api/auth/verify/route.ts:42:36)\\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\\n at Module.wrappedHandler (/workspaces/Commitlabs-Frontend/src/lib/backend/withApiHandler.ts:48:24)\\n at /workspaces/Commitlabs-Frontend/src/app/api/auth/verify/route.test.ts:155:17\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/auth/verify/route.test.ts\u001b[2m 12/12\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m21 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m530 passed\u001b[39m\u001b[22m\u001b[90m (530)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.92s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/auth/verify/route.test.ts \u001b[2m(\u001b[22m\u001b[2m12 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 37\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/auth/verify/route.test.ts\u001b[2m 12/12\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m21 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m530 passed\u001b[39m\u001b[22m\u001b[90m (530)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m6.92s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/commitmentSSE.test.ts\u001b[2m 0/7\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m21 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m530 passed\u001b[39m\u001b[22m\u001b[90m (537)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.12s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/commitmentSSE.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/[id]/events\u001b[2m > \u001b[22m\u001b[2mreturns 401 when request is not authenticated\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.447Z\",\"requestId\":\"5c81221d-9578-48b7-90c6-17fdd6b095de\",\"context\":{\"correlationId\":\"08337821ae84a0b4cc6af97d67688f87\",\"code\":\"UNAUTHORIZED\",\"status\":401,\"message\":\"No session token provided\",\"url\":\"http://localhost/api/commitments/cmt-123/events\",\"method\":\"GET\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | tests/api/commitmentSSE.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/commitments/[id]/events\u001b[2m > \u001b[22m\u001b[2mreturns 404 when commitment does not exist\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:25.463Z\",\"requestId\":\"8902ed51-ff07-4a19-8ec5-fe8dd7d85349\",\"context\":{\"correlationId\":\"35785fef93c3fc0b780dca0f91270f11\",\"code\":\"NOT_FOUND\",\"status\":404,\"message\":\"Commitment not found.\",\"url\":\"http://localhost/api/commitments/non-existent/events\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/rateLimit.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m22 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m537 passed\u001b[39m\u001b[22m\u001b[90m (537)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.32s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/commitmentSSE.test.ts \u001b[2m(\u001b[22m\u001b[2m7 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 42\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/rateLimit.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m22 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m537 passed\u001b[39m\u001b[22m\u001b[90m (537)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.32s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | tests/api/rateLimit.test.ts\u001b[2m > \u001b[22m\u001b[2mcheckRateLimit\u001b[2m > \u001b[22m\u001b[2mfails open (allows request) when KV throws\n", + "\u001b[22m\u001b[39m[RateLimit] Error checking rate limit for api/commitments/create: Error: Redis connection failed\n", + " at \u001b[90m/workspaces/Commitlabs-Frontend/\u001b[39mtests/api/rateLimit.test.ts:120:35\n", + " at \u001b[90mfile:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:302:11\n", + " at \u001b[90mfile:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:1903:26\n", + " at \u001b[90mfile:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:2326:20\n", + " at new Promise ()\n", + " at runWithCancel \u001b[90m(file:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:2323:10\u001b[90m)\u001b[39m\n", + " at \u001b[90mfile:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:2305:20\n", + " at new Promise ()\n", + " at runWithTimeout \u001b[90m(file:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:2272:10\u001b[90m)\u001b[39m\n", + " at \u001b[90mfile:///workspaces/Commitlabs-Frontend/\u001b[39mnode_modules/\u001b[4m.pnpm\u001b[24m/@vitest+runner@4.1.6/node_modules/\u001b[4m@vitest/runner\u001b[24m/dist/chunk-artifact.js:2955:64\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/errors.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m23 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m552 passed\u001b[39m\u001b[22m\u001b[90m (552)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.52s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/rateLimit.test.ts \u001b[2m(\u001b[22m\u001b[2m15 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 12\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/errors.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m23 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m552 passed\u001b[39m\u001b[22m\u001b[90m (552)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.52s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/errors.test.ts \u001b[2m(\u001b[22m\u001b[2m29 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 8\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/csrf.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m24 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m581 passed\u001b[39m\u001b[22m\u001b[90m (581)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.73s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/csrf.test.ts \u001b[2m(\u001b[22m\u001b[2m12 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 14\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/csrf.test.ts\u001b[2m 12/12\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m25 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m593 passed\u001b[39m\u001b[22m\u001b[90m (593)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m7.93s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/jsonBodyLimit.test.ts\u001b[2m 0/10\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m25 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m593 passed\u001b[39m\u001b[22m\u001b[90m (603)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.02s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/jsonBodyLimit.test.ts \u001b[2m(\u001b[22m\u001b[2m10 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 16\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/counters/__tests__/persistent.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m26 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m603 passed\u001b[39m\u001b[22m\u001b[90m (603)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.23s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/counters/__tests__/persistent.test.ts \u001b[2m(\u001b[22m\u001b[2m7 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 6\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/auth/nonce/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m27 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m610 passed\u001b[39m\u001b[22m\u001b[90m (610)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.33s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/auth/nonce/route.test.ts\u001b[2m 0/8\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m27 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m610 passed\u001b[39m\u001b[22m\u001b[90m (618)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.53s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/app/api/auth/nonce/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/nonce\u001b[2m > \u001b[22m\u001b[2mreturns 429 when rate limited\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:26.812Z\",\"requestId\":\"ddda34b1-f623-46e8-a043-327febc038a4\",\"context\":{\"correlationId\":\"98453bbee28765032bd3e27d2affe4f1\",\"code\":\"TOO_MANY_REQUESTS\",\"status\":429,\"message\":\"Rate limit exceeded for your IP. Please try again later.\",\"url\":\"http://localhost:3000/api/auth/nonce\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/nonce/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/nonce\u001b[2m > \u001b[22m\u001b[2mreturns 400 for invalid JSON body\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:26.815Z\",\"requestId\":\"be696872-24dc-4bfc-9635-4d15bc817641\",\"context\":{\"correlationId\":\"02b7ece0fb5412894243c0d123c1a308\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid JSON in request body\",\"url\":\"http://localhost:3000/api/auth/nonce\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/nonce/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/nonce\u001b[2m > \u001b[22m\u001b[2mreturns 400 when address is missing\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:26.817Z\",\"requestId\":\"e38fa9e8-4852-4173-9553-de639c642d57\",\"context\":{\"correlationId\":\"deb419fcd84efb0133336bd52416bcca\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid request data\",\"url\":\"http://localhost:3000/api/auth/nonce\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/nonce/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/nonce\u001b[2m > \u001b[22m\u001b[2mreturns 400 when address is empty string\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:26.818Z\",\"requestId\":\"3200f49c-89ef-4355-8de7-59e75b6b3e05\",\"context\":{\"correlationId\":\"9805889f546e3cedcd2461db44c5a82a\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid request data\",\"url\":\"http://localhost:3000/api/auth/nonce\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/nonce/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/nonce\u001b[2m > \u001b[22m\u001b[2mreturns 400 when body is null\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:26.820Z\",\"requestId\":\"59a09bc2-ba46-4762-9db1-9467faa5d5eb\",\"context\":{\"correlationId\":\"32abb4ffdd2abea5311ed28c400c9293\",\"code\":\"VALIDATION_ERROR\",\"status\":400,\"message\":\"Invalid request data\",\"url\":\"http://localhost:3000/api/auth/nonce\",\"method\":\"POST\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/app/api/auth/nonce/route.test.ts\u001b[2m > \u001b[22m\u001b[2mPOST /api/auth/nonce\u001b[2m > \u001b[22m\u001b[2mreturns 500 on unexpected handler error\n", + "\u001b[22m\u001b[39m{\"level\":\"error\",\"message\":\"[API] Unhandled exception\",\"timestamp\":\"2026-05-28T12:35:26.822Z\",\"requestId\":\"0ec6abf9-0961-420d-b8d6-75e8b5cd7cae\",\"context\":{\"correlationId\":\"d600f88e6d38e53d24746f465fb78cc5\",\"url\":\"http://localhost:3000/api/auth/nonce\",\"method\":\"POST\"},\"error\":{\"name\":\"Error\",\"message\":\"boom\",\"stack\":\"Error: boom\\n at /workspaces/Commitlabs-Frontend/src/app/api/auth/nonce/route.test.ts:113:63\\n at Mock (file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+spy@4.1.6/node_modules/@vitest/spy/dist/index.js:332:34)\\n at POST.cors (/workspaces/Commitlabs-Frontend/src/app/api/auth/nonce/route.ts:46:17)\\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\\n at Module.wrappedHandler (/workspaces/Commitlabs-Frontend/src/lib/backend/withApiHandler.ts:48:24)\\n at /workspaces/Commitlabs-Frontend/src/app/api/auth/nonce/route.test.ts:115:17\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:20\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/notifications.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m28 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m618 passed\u001b[39m\u001b[22m\u001b[90m (618)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.63s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/auth/nonce/route.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 26\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/notifications.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m28 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m618 passed\u001b[39m\u001b[22m\u001b[90m (618)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.63s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/notifications.test.ts \u001b[2m(\u001b[22m\u001b[2m5 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 15\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/counters/__tests__/inMemory.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m29 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m623 passed\u001b[39m\u001b[22m\u001b[90m (623)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m8.93s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/counters/__tests__/inMemory.test.ts \u001b[2m(\u001b[22m\u001b[2m7 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 6\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/storage.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m30 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m630 passed\u001b[39m\u001b[22m\u001b[90m (630)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.03s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/lib/backend/storage.test.ts\u001b[2m > \u001b[22m\u001b[2mKeyValueStorageAdapter\u001b[2m > \u001b[22m\u001b[2mreturns a safe error when the external client is unavailable\n", + "\u001b[22m\u001b[39m{\"level\":\"error\",\"message\":\"[Storage] get failed\",\"timestamp\":\"2026-05-28T12:35:27.442Z\",\"error\":{\"name\":\"Error\",\"message\":\"ECONNREFUSED redis://secret\",\"stack\":\"Error: ECONNREFUSED redis://secret\\n at /workspaces/Commitlabs-Frontend/src/lib/backend/storage.test.ts:83:38\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:302:11\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:1903:26\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2326:20\\n at new Promise ()\\n at runWithCancel (file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2323:10)\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2305:20\\n at new Promise ()\\n at runWithTimeout (file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2272:10)\\n at file:///workspaces/Commitlabs-Frontend/node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/chunk-artifact.js:2955:64\"}}\n", + "\n", + "\u001b[90mstderr\u001b[2m | src/lib/backend/storage.test.ts\u001b[2m > \u001b[22m\u001b[2mcreateStorageAdapter\u001b[2m > \u001b[22m\u001b[2mfalls back to memory storage when an external provider is requested without a client\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[Storage] External storage provider requested without a configured client. Falling back to memory storage.\",\"timestamp\":\"2026-05-28T12:35:27.447Z\",\"context\":{\"provider\":\"redis\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/metrics.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m31 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m636 passed\u001b[39m\u001b[22m\u001b[90m (636)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.23s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/storage.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 15\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/metrics.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m31 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m636 passed\u001b[39m\u001b[22m\u001b[90m (636)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.23s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/api/metrics.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 12\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/utils/response.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m32 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m638 passed\u001b[39m\u001b[22m\u001b[90m (638)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.53s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/utils/response.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 30\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/withApiHandler.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m33 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m642 passed\u001b[39m\u001b[22m\u001b[90m (642)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.63s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstderr\u001b[2m | src/lib/backend/withApiHandler.test.ts\u001b[2m > \u001b[22m\u001b[2mwithApiHandler\u001b[2m > \u001b[22m\u001b[2mconverts ApiError instances into JSON responses and preserves CORS headers\n", + "\u001b[22m\u001b[39m{\"level\":\"warn\",\"message\":\"[API] Handled error\",\"timestamp\":\"2026-05-28T12:35:28.062Z\",\"requestId\":\"6aa2c5bd-044d-449b-8fe9-e153869ec558\",\"context\":{\"correlationId\":\"6b944315278da4fc936928e4be57e394\",\"code\":\"FORBIDDEN\",\"status\":403,\"message\":\"Blocked by policy\",\"url\":\"http://localhost:3000/api/example\",\"method\":\"GET\"}}\n", + "\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/session.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m34 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m644 passed\u001b[39m\u001b[22m\u001b[90m (644)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.93s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/withApiHandler.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 13\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/session.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m34 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m644 passed\u001b[39m\u001b[22m\u001b[90m (644)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m9.93s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/session.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 13\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/lib/csv.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m35 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m650 passed\u001b[39m\u001b[22m\u001b[90m (650)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m10.13s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m tests/lib/csv.test.ts \u001b[2m(\u001b[22m\u001b[2m10 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 5\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/config-supported.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m36 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m660 passed\u001b[39m\u001b[22m\u001b[90m (660)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m10.23s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22mtests/api/config-supported.test.ts\u001b[2m 0/2\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m36 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m660 passed\u001b[39m\u001b[22m\u001b[90m (662)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m10.33s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[90mstdout\u001b[2m | tests/api/config-supported.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/config/supported\u001b[2m > \u001b[22m\u001b[2mshould return 200 with supported config data\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Supported config requested\",\"timestamp\":\"2026-05-28T12:35:28.686Z\",\"requestId\":\"172e4c38-2d68-4fea-8b72-d8b8b4ef82ac\"}\n", + "\n", + "\u001b[90mstdout\u001b[2m | tests/api/config-supported.test.ts\u001b[2m > \u001b[22m\u001b[2mGET /api/config/supported\u001b[2m > \u001b[22m\u001b[2mshould match the source of truth config module\n", + "\u001b[22m\u001b[39m{\"level\":\"info\",\"message\":\"Supported config requested\",\"timestamp\":\"2026-05-28T12:35:28.692Z\",\"requestId\":\"c8878337-5452-435c-a08f-a51f579d6f96\"}\n", + "\n", + " \u001b[32m✓\u001b[39m tests/api/config-supported.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 14\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/lib/backend/sessionCookies.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m37 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m662 passed\u001b[39m\u001b[22m\u001b[90m (662)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m10.53s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/lib/backend/sessionCookies.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 7\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/marketplace/listings/[id]/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m38 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m664 passed\u001b[39m\u001b[22m\u001b[90m (664)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m10.74s\n", + "\u001b[?2026l\u001b[?2026h\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/marketplace/listings/[id]/route.test.ts \u001b[2m(\u001b[22m\u001b[2m1 test\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 4\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[1m\u001b[33m ❯ \u001b[39m\u001b[22msrc/app/api/marketplace/listings/route.test.ts\u001b[2m [queued]\u001b[22m\n", + "\n", + "\u001b[2m Test Files \u001b[22m\u001b[1m\u001b[32m39 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m\u001b[1m\u001b[32m665 passed\u001b[39m\u001b[22m\u001b[90m (665)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m12:35:18\n", + "\u001b[2m Duration \u001b[22m10.84s\n", + "\u001b[?2026l\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K\u001b[1A\u001b[K \u001b[32m✓\u001b[39m src/app/api/marketplace/listings/route.test.ts \u001b[2m(\u001b[22m\u001b[2m1 test\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 3\u001b[2mms\u001b[22m\u001b[39m\n", + "\n", + "\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m40 passed\u001b[39m\u001b[22m\u001b[90m (40)\u001b[39m\n", + "\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m666 passed\u001b[39m\u001b[22m\u001b[90m (666)\u001b[39m\n", + "\u001b[2m Start at \u001b[22m 12:35:18\n", + "\u001b[2m Duration \u001b[22m 10.93s\u001b[2m (transform 1.14s, setup 0ms, import 3.22s, tests 1.64s, environment 4ms)\u001b[22m\n", + "\n", + "\u001b[1m\u001b[30m\u001b[42m PASS \u001b[49m\u001b[39m\u001b[22m \u001b[32mWaiting for file changes...\u001b[39m\n", + " \u001b[2mpress \u001b[22m\u001b[1mh\u001b[22m\u001b[2m to show help\u001b[22m\u001b[2m, \u001b[22m\u001b[2mpress \u001b[22m\u001b[1mq\u001b[22m\u001b[2m to quit\u001b[22m\n" + ] + } + ], + "source": [ + "!cd /workspaces/Commitlabs-Frontend && pnpm test -- --run" + ] + }, + { + "cell_type": "markdown", + "id": "479b7710", + "metadata": {}, + "source": [ + "## 3. Run Linters and Formatters\n", + "Run linting and format validation to ensure code style and quality checks pass." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58f4a91d", + "metadata": {}, + "outputs": [], + "source": [ + "!cd /workspaces/Commitlabs-Frontend && pnpm lint" + ] + }, + { + "cell_type": "markdown", + "id": "d89ece71", + "metadata": {}, + "source": [ + "## 4. Build the Project\n", + "Run the Next.js production build to verify the frontend compiles successfully." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51f1d7c6", + "metadata": {}, + "outputs": [], + "source": [ + "!cd /workspaces/Commitlabs-Frontend && pnpm build" + ] + }, + { + "cell_type": "markdown", + "id": "c87a975c", + "metadata": {}, + "source": [ + "## 5. Verify with Smoke Test\n", + "Optionally run a quick smoke test or basic app start to confirm the project starts without runtime issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7772776c", + "metadata": {}, + "outputs": [], + "source": [ + "!cd /workspaces/Commitlabs-Frontend && pnpm start & sleep 5 && pkill -f \"next start\"" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/verify-project.ipynb b/verify-project.ipynb new file mode 100644 index 00000000..21c26798 --- /dev/null +++ b/verify-project.ipynb @@ -0,0 +1,10 @@ +{ + "cells": [], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}