Trader Agent Test Suite Specification
April 25, 2026 ยท View on GitHub
Version: 0.1 Date: 2026-04-03 Status: Draft Covers: TIP-0 v0.2, NP-0 v0.1, Extended ACP Commands, SwapModule Integration References: Protocol Spec | Architecture | Integration Guide
Test Infrastructure Notes
All tests follow the patterns established in the existing E2E suite (test/e2e/). Key conventions:
- Tests use
vitestwithdescribe/it/expect. - Each test group uses a
setup*()helper that returns a context object with mocked dependencies. - Cleanup runs in
afterEachblocks, including stopping listeners, unsubscribing DMs, disposing managers, and checking for unexpected DM handler errors. - Mock objects:
MockMarketModule,MockSwapModule,MockPaymentsModule,MockCommunicationsModule,MockVolumeReservationLedger,MockTraderStateStore. - Time control: Tests use
vi.useFakeTimers()for timeout and expiry scenarios. - Crypto stubs: Deterministic keypairs (
PK_TRADER_A,PK_TRADER_B,PK_TRADER_C) with known pubkey ordering for proposer selection tests.
Category 1: Intent Lifecycle (Happy Path)
T1.1: Create intent and verify publication to MarketModule
Test name: should create intent and publish to MarketModule via postIntent()
Preconditions:
- Trader agent spawned and in RUNNING state.
- MockPaymentsModule returns balance of 1000 ALPHA.
- MockMarketModule.postIntent() returns
{ id: 'mkt-001' }.
Steps:
- Send ACP command
CREATE_INTENTwith params:{ direction: 'sell', base_asset: 'ALPHA', quote_asset: 'USDC', rate_min: 450, rate_max: 500, volume_min: 100, volume_max: 1000, expiry_sec: 86400 }. - Await
acp.resultresponse.
Expected outcome:
- Response contains
intent_id(64 hex chars),market_intent_id: 'mkt-001',state: 'ACTIVE',expiry_msapproximatelynow + 86400000. - MockMarketModule.postIntent() was called once with
intentType: 'sell',category: 'ALPHA/USDC',price: 475(midpoint),currency: 'USDC',contactHandlematching agent address,expiresInDays: 1. - Description matches canonical format:
"Selling 100-1000 ALPHA for USDC. Rate: 450-500 USDC per ALPHA. Escrow: any. Deposit timeout: 300s.". - Intent persisted to TraderStateStore.
Category: Intent Lifecycle
T1.2: Create intent with unique intent_id (salt ensures uniqueness)
Test name: should produce unique intent_id for identical params due to random salt
Preconditions:
- Trader agent spawned. Balance of 2000 ALPHA.
- MockMarketModule.postIntent() succeeds.
Steps:
- Send
CREATE_INTENTwith identical params twice (same direction, assets, rates, volumes, expiry). - Collect both
intent_idvalues from responses.
Expected outcome:
- Both intents created successfully.
intent_id_1 !== intent_id_2(different salts produce different SHA-256 hashes).- Both intents exist in TraderStateStore with state
ACTIVE.
Category: Intent Lifecycle
T1.3: Create intent with all optional fields specified
Test name: should create intent with explicit escrow_address and deposit_timeout_sec
Preconditions:
- Trader agent spawned. Balance of 500 ALPHA.
- A known escrow pubkey
ESCROW_PK(valid secp256k1 compressed key).
Steps:
- Send
CREATE_INTENTwith:{ direction: 'sell', base_asset: 'ALPHA', quote_asset: 'USDC', rate_min: 450, rate_max: 500, volume_min: 100, volume_max: 500, escrow_address: ESCROW_PK, deposit_timeout_sec: 120, expiry_sec: 3600 }.
Expected outcome:
- Intent created with
escrow_address: ESCROW_PK(not"any"). deposit_timeout_sec: 120.- Description includes
Escrow: <ESCROW_PK>. Deposit timeout: 120s.. expiry_msapproximatelynow + 3600000.
Category: Intent Lifecycle
T1.4: Cancel active intent
Test name: should cancel active intent and close on MarketModule
Preconditions:
- Trader agent spawned. Intent
I1created and inACTIVEstate withmarket_intent_id: 'mkt-001'.
Steps:
- Send
CANCEL_INTENTwith{ intent_id: I1.intent_id, reason: 'no longer needed' }. - Await
acp.resultresponse.
Expected outcome:
- Response:
{ intent_id: I1.intent_id, state: 'CANCELLED', volume_filled: 0 }. - MockMarketModule.closeIntent() called with
'mkt-001'. - Intent state in TraderStateStore is
CANCELLED. - No further matching attempts for this intent.
Category: Intent Lifecycle
T1.5: Intent expires and is automatically removed
Test name: should transition intent to EXPIRED when expiry_ms is reached
Preconditions:
- Trader agent spawned. Intent
I1created withexpiry_sec: 10(expires in 10 seconds). vi.useFakeTimers().
Steps:
- Verify intent state is
ACTIVE. - Advance time by 11 seconds.
- Trigger or await expiry sweep (runs every 10 seconds per spec).
- Query intent state.
Expected outcome:
- Intent state transitioned to
EXPIRED. - MockMarketModule.closeIntent() called with reason indicating expiry.
- Intent no longer considered for matching.
Category: Intent Lifecycle
T1.6: List intents with various filters
Test name: should filter intents by state via LIST_INTENTS command
Preconditions:
- Trader agent spawned with 4 intents:
I_ACTIVE: state ACTIVEI_FILLED: state FILLED (volume_filled === volume_max)I_CANCELLED: state CANCELLEDI_EXPIRED: state EXPIRED
Steps:
- Send
LIST_INTENTSwith{ filter: 'active' }. Assert result contains onlyI_ACTIVE. - Send
LIST_INTENTSwith{ filter: 'filled' }. Assert result contains onlyI_FILLED. - Send
LIST_INTENTSwith{ filter: 'cancelled' }. Assert result contains onlyI_CANCELLED. - Send
LIST_INTENTSwith{ filter: 'expired' }. Assert result contains onlyI_EXPIRED. - Send
LIST_INTENTSwith{ filter: 'all' }. Assert result contains all 4. - Send
LIST_INTENTSwith no filter. Assert result contains all 4 (default is'all').
Expected outcome:
- Each filter returns exactly the matching intents.
- Response includes
totalcount andintentsarray withIntentSummaryfields. - Pagination works:
LIST_INTENTSwith{ filter: 'all', limit: 2, offset: 0 }returns first 2; withoffset: 2returns next 2.
Category: Intent Lifecycle
T1.7: Create multiple intents for different asset pairs
Test name: should support intents across multiple asset pairs simultaneously
Preconditions:
- Trader agent spawned. Balance: 1000 ALPHA, 500 BTC_L2, 10000 USDC.
Steps:
- Create intent: sell 500 ALPHA for USDC at rate 450-500.
- Create intent: buy 100 BTC_L2 for USDC at rate 25000-26000.
- Create intent: sell 200 BTC_L2 for ALPHA at rate 50-55.
- Send
LIST_INTENTSwith{ filter: 'active' }.
Expected outcome:
- All 3 intents created with unique
intent_idvalues. LIST_INTENTSreturns 3 active intents with correct asset pairs.- Each posted to MarketModule with correct
category(ALPHA/USDC,BTC_L2/USDC,BTC_L2/ALPHA).
Category: Intent Lifecycle
Category 2: Intent Matching
T2.1: Buy intent matches counterparty sell intent via search()
Test name: should match buy intent against counterparty sell intent found via search()
Preconditions:
- Trader A spawned with buy intent: ALPHA/USDC, rate 450-500, volume 100-500.
- MockMarketModule.search() returns one result from Trader B: sell ALPHA/USDC, rate 460-490, volume 200-800, agent_pubkey = PK_TRADER_B.
Steps:
- IntentEngine scan loop fires.
- search() called with query derived from Trader A's buy intent.
- Result parsed, client-side matching validates: opposite direction, same pair, overlapping rates [460, 490], volume sufficient.
Expected outcome:
- Match detected.
- Agreed rate = floor((460 + 490) / 2) = 475.
- Agreed volume = min(500, 800) = 500.
- Volume reserved via VolumeReservationLedger.
- Intent transitions ACTIVE -> MATCHING.
- NegotiationHandler invoked with match details.
Category: Intent Matching
T2.2: Match with overlapping rate ranges computes midpoint correctly
Test name: should compute agreed rate as floor of midpoint of overlapping range
Preconditions:
- Intent A: buy, rate_min=400, rate_max=500.
- Intent B: sell, rate_min=450, rate_max=550.
Steps:
- Evaluate match. Overlap: [450, 500].
- Compute agreed_rate.
Expected outcome:
overlap_min = max(400, 450) = 450.overlap_max = min(500, 550) = 500.agreed_rate = floor((450 + 500) / 2) = 475.
Category: Intent Matching
T2.3: Match with exact rate overlap (single point)
Test name: should match when rate ranges overlap at exactly one point
Preconditions:
- Intent A: buy, rate_min=400, rate_max=450.
- Intent B: sell, rate_min=450, rate_max=500.
Steps:
- Evaluate matching criteria.
Expected outcome:
- Overlap: [450, 450]. Single point.
agreed_rate = floor((450 + 450) / 2) = 450.- Match succeeds.
Category: Intent Matching
T2.4: No match when volume is below volume_min
Test name: should not match when available volume is below both intents volume_min
Preconditions:
- Intent A: buy, volume_min=200, volume_max=500, volume_filled=400 (available=100).
- Intent B: sell, volume_min=150, volume_max=300 (available=300).
Steps:
- Evaluate matching criteria.
min(100, 300) = 100 < max(200, 150) = 200.
Expected outcome:
- Match fails criterion 4 (insufficient volume).
- No negotiation initiated.
- No volume reserved.
Category: Intent Matching
T2.5: Partial volume coverage initiates partial fill
Test name: should initiate partial fill when counterparty available volume < own volume_max
Preconditions:
- Intent A: sell, volume_min=100, volume_max=1000, volume_filled=0.
- Intent B: buy, volume_min=50, volume_max=300, volume_filled=0.
Steps:
- Match evaluates.
min(1000, 300) = 300 >= max(100, 50) = 100. Match succeeds.
Expected outcome:
agreed_volume = 300.- Negotiation initiated for 300 units (partial fill of A's 1000).
- After successful swap, A transitions to PARTIALLY_FILLED with volume_filled=300.
Category: Intent Matching
T2.6: Deterministic proposer selection (lower pubkey proposes)
Test name: should select agent with lexicographically lower pubkey as proposer
Preconditions:
- PK_TRADER_A =
"02aaa..."(lexicographically lower). - PK_TRADER_B =
"02bbb..."(lexicographically higher). - Both agents discover each other's intents simultaneously.
Steps:
- Trader A evaluates match with Trader B's intent. A's pubkey < B's pubkey.
- Trader B evaluates match with Trader A's intent. B's pubkey > A's pubkey.
Expected outcome:
- Trader A sends
np.propose_deal(is proposer). - Trader B does NOT send
np.propose_deal; waits for incoming proposal. - Only one
np.propose_dealis sent for this intent pair.
Category: Intent Matching
T2.7: Self-matching prevention
Test name: should filter out own intents from search results
Preconditions:
- Trader A has a sell intent and a buy intent for the same pair that overlap in rate.
- MockMarketModule.search() returns Trader A's own sell intent (same agent_pubkey).
Steps:
- IntentEngine processes search results.
Expected outcome:
- Result with
agentPublicKey === agent_pubkeyis filtered out (criterion 8). - No match initiated against own intent.
Category: Intent Matching
T2.8: Match via subscribeFeed() real-time notification
Test name: should detect match via MarketModule.subscribeFeed() callback
Preconditions:
- Trader A has an active buy intent for ALPHA/USDC.
- subscribeFeed() registered with onIntent callback.
Steps:
- Feed callback fires with FeedListing from Trader B (sell ALPHA/USDC).
- IntentEngine calls search() to get full details.
- Client-side matching validates the result.
Expected outcome:
- Match detected via feed path (not periodic scan).
- search() called after feed event to retrieve full intent details.
- Normal matching flow proceeds (reserve volume, start negotiation).
Category: Intent Matching
T2.9: Match via periodic search() scan
Test name: should discover matching intents via periodic search() scan loop
Preconditions:
- Trader A has active sell intent for ALPHA/USDC.
- Scan loop interval: 5 seconds.
- MockMarketModule.search() returns a matching buy intent.
Steps:
- Wait for scan loop to fire (or trigger manually).
- search() returns matching result.
Expected outcome:
- IntentEngine called search() with correctly constructed query and filters.
intentTypefilter set to opposite direction ('buy').categoryfilter set to'ALPHA/USDC'.minPrice/maxPricefilters set from own intent's rate range.- Match detected and negotiation initiated.
Category: Intent Matching
Category 3: NP-0 Negotiation (Happy Path)
T3.1: Propose deal then accept deal leading to swap
Test name: should complete NP-0 negotiation: propose_deal -> accept_deal -> proceed to swap
Preconditions:
- Trader A matched with Trader B's intent. Volume reserved.
- Trader A is the proposer (lower pubkey).
- NP-0 DM transport mocked.
Steps:
- Trader A sends
np.propose_dealwith DealTerms:{ proposer_pubkey: PK_A, acceptor_pubkey: PK_B, rate: 475, volume: 300, escrow_address: 'any', deposit_timeout_sec: 300 }. - Trader B receives proposal, validates terms, sends
np.accept_deal. - Trader A receives acceptance.
Expected outcome:
- Deal state: PROPOSED -> ACCEPTED.
np.propose_dealenvelope has correctnp_version: '0.1', validmsg_id,deal_idmatching SHA-256 of canonical DealTerms, valid signature.np.accept_dealenvelope has matchingdeal_id,sender_pubkey === PK_B.- After acceptance, SwapExecutor invoked (pingEscrow then proposeSwap).
- Intent transitions MATCHING -> NEGOTIATING.
Category: NP-0 Negotiation
T3.2: Counter-proposal via reject then re-propose
Test name: should handle counter-proposal flow: reject with reason then re-propose
Preconditions:
- Trader A proposes deal at rate 475.
- Trader B's strategy requires min_profit_margin not met at 475.
Steps:
- Trader A sends
np.propose_dealat rate 475. - Trader B sends
np.reject_dealwith reasonRATE_UNACCEPTABLE. - Trader A's intent returns to ACTIVE, volume released.
- Trader A re-evaluates match with adjusted parameters.
- (If rate range still overlaps) Trader A sends new
np.propose_dealat a different rate.
Expected outcome:
- First deal enters CANCELLED state.
- Volume reservation released after rejection.
- Intent transitions NEGOTIATING -> ACTIVE.
- New deal created with fresh
deal_id. - Intent transitions ACTIVE -> MATCHING -> NEGOTIATING again.
Category: NP-0 Negotiation
T3.3: Reject deal with each reason code
Test name: should accept np.reject_deal with each valid DealRejectReason
Preconditions:
- Active deal in PROPOSED state.
Steps:
- For each reason code in
[RATE_UNACCEPTABLE, VOLUME_UNACCEPTABLE, ESCROW_UNACCEPTABLE, TIMEOUT_UNACCEPTABLE, INSUFFICIENT_BALANCE, STRATEGY_MISMATCH, AGENT_BUSY, OTHER]: a. Sendnp.reject_dealwith thatreason_code. b. Verify deal transitions to CANCELLED. c. Verify volume released.
Expected outcome:
- All 8 reason codes are accepted as valid.
- Deal transitions to CANCELLED for each.
- Rejection logged with reason code and optional message.
Category: NP-0 Negotiation
Category 4: NP-0 Negotiation (Unhappy Path)
T4.1: Proposal timeout (30s)
Test name: should cancel deal after 30s proposal timeout with no response
Preconditions:
- Trader A sends
np.propose_deal. Deal in PROPOSED state. vi.useFakeTimers().
Steps:
- Advance time by 30 seconds.
- No
np.accept_dealornp.reject_dealreceived.
Expected outcome:
- Deal transitions PROPOSED -> CANCELLED.
- Volume reservation released.
- Intent returns to ACTIVE for re-matching.
- Timeout logged.
Category: NP-0 Negotiation (Unhappy)
T4.2: Acceptance timeout (60s)
Test name: should cancel deal after 60s acceptance timeout
Preconditions:
- Deal in ACCEPTED state (post np.accept_deal).
vi.useFakeTimers().
Steps:
- Advance time by 60 seconds.
- No pingEscrow/proposeSwap completion.
Expected outcome:
- Deal transitions ACCEPTED -> CANCELLED.
- Volume reservation released.
- Intent returns to ACTIVE.
Category: NP-0 Negotiation (Unhappy)
T4.3: Counterparty goes offline mid-negotiation
Test name: should handle counterparty going offline via timeout mechanism
Preconditions:
- Deal in PROPOSED state. Counterparty DM delivery fails silently (Nostr relay best-effort).
Steps:
- Wait for 30s proposal timeout to fire.
Expected outcome:
- Same as T4.1: deal cancelled via timeout.
- No crash or unhandled rejection.
Category: NP-0 Negotiation (Unhappy)
T4.4: Invalid NP-0 message signature
Test name: should reject NP-0 message with invalid ECDSA signature
Preconditions:
- Deal in PROPOSED state.
- Incoming
np.accept_dealwith corrupted signature field.
Steps:
- Receive NP-0 message with valid envelope but signature that does not verify against sender_pubkey.
Expected outcome:
- Message rejected silently (logged as warning).
- Deal state remains PROPOSED.
- No state transition.
Category: NP-0 Negotiation (Unhappy)
T4.5: NP-0 message with wrong deal_id
Test name: should reject NP-0 message referencing unknown deal_id
Preconditions:
- Deal
D1in PROPOSED state withdeal_id = 'aaa...'.
Steps:
- Receive
np.accept_dealwithdeal_id = 'bbb...'(not matching any active deal).
Expected outcome:
- Message discarded. Logged as warning: unknown deal_id.
- No state change to any deal.
Category: NP-0 Negotiation (Unhappy)
T4.6: NP-0 message from wrong sender_pubkey
Test name: should reject NP-0 message from non-participant pubkey
Preconditions:
- Deal between PK_A (proposer) and PK_B (acceptor) in PROPOSED state.
- PK_C is a third party.
Steps:
- Receive
np.accept_dealwithsender_pubkey = PK_C(not a participant in the deal).
Expected outcome:
- Message rejected: sender_pubkey is not proposer or acceptor.
- Deal state unchanged.
- Security event logged.
Category: NP-0 Negotiation (Unhappy)
T4.7: Duplicate proposal for same intent (AGENT_BUSY)
Test name: should reject second proposal for intent with active deal
Preconditions:
- Trader B has intent
I_Bwith an active deal (state PROPOSED or EXECUTING). - Trader C sends
np.propose_dealreferencingI_Bas acceptor_intent_id.
Steps:
- Receive
np.propose_dealfrom Trader C for intentI_B.
Expected outcome:
- Trader B sends
np.reject_dealwithreason_code: 'AGENT_BUSY'. - Existing deal remains unaffected.
Category: NP-0 Negotiation (Unhappy)
T4.8: NP-0 message exceeding 64KB
Test name: should reject NP-0 message exceeding 64 KiB size limit
Preconditions:
- Deal in PROPOSED state.
Steps:
- Construct an
np.accept_dealwithmessagefield padded to exceed 64 KiB total. - Deliver to agent.
Expected outcome:
- Message rejected before parsing payload.
- Deal state unchanged.
- Size violation logged.
Category: NP-0 Negotiation (Unhappy)
T4.9: Malformed NP-0 JSON
Test name: should reject malformed JSON in NP-0 message
Preconditions:
- DM transport delivers a message that is not valid JSON.
Steps:
- Deliver
"{not valid json"as NP-0 message body.
Expected outcome:
- Parse error caught. No crash.
- Warning logged.
- No state changes.
Category: NP-0 Negotiation (Unhappy)
T4.10: NP-0 message with dangerous keys (proto)
Test name: should reject NP-0 message containing prototype pollution keys
Preconditions:
- Deal in PROPOSED state.
Steps:
- Deliver
np.accept_dealwith injected key:{ "__proto__": { "isAdmin": true }, ... }. - Also test
constructorandprototypekeys at nested levels.
Expected outcome:
- Message rejected by dangerous key check (consistent with
hasDangerousKeys()in envelope.ts). - No prototype pollution.
- Security event logged.
Category: NP-0 Negotiation (Unhappy)
Category 5: Swap Execution (Happy Path)
T5.1: Full swap flow from negotiation to completion
Test name: should complete full swap: negotiate -> pingEscrow -> proposeSwap -> acceptSwap -> deposit -> payout -> completed
Preconditions:
- Deal in ACCEPTED state between Trader A (proposer) and Trader B (acceptor).
- MockSwapModule.pingEscrow() resolves successfully.
- MockSwapModule.proposeSwap() returns
{ swapId: 'swap-001' }.
Steps:
- SwapExecutor calls pingEscrow(escrowAddress, 10000). Succeeds.
- Deal transitions ACCEPTED -> EXECUTING.
- SwapExecutor calls proposeSwap(deal).
- Counterparty receives
swap:proposal_received, calls acceptSwap(). - MockSwapModule emits
swap:completedwith{ swapId: 'swap-001', payoutVerified: true }.
Expected outcome:
- Deal transitions EXECUTING -> COMPLETED.
- Volume reservation released via VolumeReservationLedger.
- Intent
volume_filledupdated by agreed volume amount. - Deal persisted to TraderStateStore with terminal state COMPLETED.
Category: Swap Execution
T5.2: Payout verification passes (payoutVerified === true)
Test name: should only update volume_filled when payoutVerified is true
Preconditions:
- Deal in EXECUTING state. Agreed volume = 300.
Steps:
- MockSwapModule emits
swap:completedwithpayoutVerified: true.
Expected outcome:
volume_filledincreases by 300.- Reservation released.
- Deal enters COMPLETED.
Category: Swap Execution
T5.3: Volume_filled updated correctly after completion
Test name: should correctly accumulate volume_filled across multiple partial fills
Preconditions:
- Intent with volume_max=1000, volume_filled=0.
- First deal: volume=400. Second deal: volume=300.
Steps:
- Complete first swap (payoutVerified=true, volume=400).
- Verify volume_filled=400, intent state=PARTIALLY_FILLED.
- Complete second swap (payoutVerified=true, volume=300).
- Verify volume_filled=700, intent state=PARTIALLY_FILLED.
Expected outcome:
- volume_filled is cumulative: 0 -> 400 -> 700.
- Intent remains PARTIALLY_FILLED (700 < 1000).
- Available volume for next match: 300.
Category: Swap Execution
T5.4: VolumeReservationLedger released after completion
Test name: should release volume reservation upon swap completion
Preconditions:
- Balance: 1000 ALPHA. Reservation of 400 for deal D1.
- getAvailable('ALPHA') returns 600.
Steps:
- Deal D1 swap completes (swap:completed, payoutVerified=true).
- Check getAvailable('ALPHA').
Expected outcome:
- Reservation for D1 released.
- getAvailable('ALPHA') returns 1000 (full balance minus no reservations).
- getReservations() does not contain D1.
Category: Swap Execution
T5.5: Intent transitions to FILLED when fully filled
Test name: should transition intent to FILLED when volume_filled reaches volume_max
Preconditions:
- Intent with volume_max=500, volume_filled=200.
- Deal for remaining 300 completes.
Steps:
- Swap completes with volume=300, payoutVerified=true.
- volume_filled = 200 + 300 = 500 = volume_max.
Expected outcome:
- Intent state transitions to FILLED.
- MockMarketModule.closeIntent() called.
- No further matching attempted for this intent.
Category: Swap Execution
T5.6: MarketModule.closeIntent() called on full fill
Test name: should call MarketModule.closeIntent() when intent is fully filled
Preconditions:
- Intent with market_intent_id='mkt-001'. About to be fully filled.
Steps:
- Swap completes, volume_filled reaches volume_max.
Expected outcome:
- MockMarketModule.closeIntent() called exactly once with
'mkt-001'. - Intent state is FILLED.
Category: Swap Execution
Category 6: Swap Execution (Unhappy Path)
T6.1: pingEscrow fails leading to FAILED with ESCROW_UNREACHABLE
Test name: should fail deal with ESCROW_UNREACHABLE when pingEscrow() rejects
Preconditions:
- Deal in ACCEPTED state.
- MockSwapModule.pingEscrow() rejects with timeout error.
Steps:
- SwapExecutor calls pingEscrow(escrowAddress, 10000).
- Call rejects/times out.
Expected outcome:
- Deal transitions ACCEPTED -> FAILED with reason
ESCROW_UNREACHABLE. - Volume reservation released.
- Intent returns to ACTIVE for re-matching.
- Failure logged.
Category: Swap Execution (Unhappy)
T6.2: proposeSwap times out
Test name: should fail deal when SwapModule.proposeSwap() times out
Preconditions:
- Deal in EXECUTING state. proposeSwap() called but no completion event within deadline.
vi.useFakeTimers().
Steps:
- Advance time by
deposit_timeout_sec + 60seconds.
Expected outcome:
- Deal transitions EXECUTING -> FAILED.
- Volume reservation released.
- Intent returns to ACTIVE.
Category: Swap Execution (Unhappy)
T6.3: Counterparty rejects swap proposal
Test name: should handle counterparty swap rejection gracefully
Preconditions:
- Deal in EXECUTING state. proposeSwap() called.
Steps:
- MockSwapModule emits
swap:failedevent.
Expected outcome:
- Deal transitions EXECUTING -> FAILED.
- Volume reservation released.
- Intent returns to ACTIVE (if not in terminal state).
Category: Swap Execution (Unhappy)
T6.4: Deposit fails (insufficient balance after reservation)
Test name: should handle deposit failure when balance is insufficient at execution time
Preconditions:
- Volume was reserved when balance was 1000. External send reduced balance to 200. Reservation was for 500.
Steps:
- SwapModule attempts deposit. PaymentsModule.send() fails with insufficient balance.
- swap:failed event emitted.
Expected outcome:
- Deal transitions to FAILED.
- Reservation released.
- Intent returns to ACTIVE.
- Error logged with balance details.
Category: Swap Execution (Unhappy)
T6.5: Counterparty never deposits (escrow timeout)
Test name: should handle escrow deposit timeout when counterparty never deposits
Preconditions:
- Deal in EXECUTING state. Agent deposited. Counterparty does not deposit.
vi.useFakeTimers().
Steps:
- Advance time past deposit_timeout_sec.
- Escrow triggers timeout, emits
swap:cancelledevent.
Expected outcome:
- Deal transitions EXECUTING -> FAILED.
- Volume reservation released.
- Agent's deposited tokens refunded by escrow.
- Intent returns to ACTIVE.
Category: Swap Execution (Unhappy)
T6.6: Escrow returns error on announce
Test name: should fail deal when escrow returns error during swap announcement
Preconditions:
- Deal in EXECUTING state.
- SwapModule's announce to escrow fails.
Steps:
- MockSwapModule emits
swap:failedwith escrow error.
Expected outcome:
- Deal transitions to FAILED.
- Reservation released.
- Intent returns to ACTIVE.
Category: Swap Execution (Unhappy)
T6.7: swap:failed event releases reservation and restores intent
Test name: should release reservation and restore intent to ACTIVE on swap:failed
Preconditions:
- Intent in NEGOTIATING state. Deal in EXECUTING state. Reservation for 500 ALPHA.
Steps:
- MockSwapModule emits
swap:failed.
Expected outcome:
- Reservation released: getAvailable('ALPHA') increases by 500.
- volume_filled NOT updated (no fill occurred).
- Intent transitions back to ACTIVE.
- Intent available for re-matching.
Category: Swap Execution (Unhappy)
T6.8: swap:cancelled event releases reservation and restores intent
Test name: should release reservation and restore intent to ACTIVE on swap:cancelled
Preconditions:
- Intent in NEGOTIATING state. Deal in EXECUTING state. Reservation for 300 ALPHA.
Steps:
- MockSwapModule emits
swap:cancelled.
Expected outcome:
- Reservation released.
- volume_filled NOT updated.
- Intent transitions back to ACTIVE.
Category: Swap Execution (Unhappy)
Category 7: Partial Fill Scenarios
T7.1: Partial fill then remaining re-posted
Test name: should handle partial fill of 400 out of 1000, leaving 600 for re-matching
Preconditions:
- Intent: sell 1000 ALPHA, volume_min=100.
- Match with Trader B for 400 units.
Steps:
- Negotiate and complete swap for 400 units.
- volume_filled = 400. remaining = 600 >= volume_min (100).
Expected outcome:
- Intent transitions to PARTIALLY_FILLED.
- Old market intent closed via closeIntent().
- New market intent posted with adjusted volume (600 remaining).
- Intent returns to ACTIVE for re-matching.
- volume_filled = 400 persisted.
Category: Partial Fill
T7.2: Multiple partial fills until fully filled
Test name: should accumulate multiple partial fills until intent is fully filled
Preconditions:
- Intent: sell 1000 ALPHA, volume_min=100.
Steps:
- Fill 1: 300 units -> volume_filled=300, PARTIALLY_FILLED, re-posted.
- Fill 2: 400 units -> volume_filled=700, PARTIALLY_FILLED, re-posted.
- Fill 3: 300 units -> volume_filled=1000, FILLED.
Expected outcome:
- Three sequential deals complete successfully.
- After fill 3: intent state = FILLED.
- MarketModule.closeIntent() called after fill 3.
- No further matching.
Category: Partial Fill
T7.3: Partial fill leaves remaining below volume_min
Test name: should treat intent as FILLED when remaining volume < volume_min
Preconditions:
- Intent: sell 1000 ALPHA, volume_min=200.
- First fill: 850 units. Remaining = 150 < volume_min (200).
Steps:
- Complete swap for 850 units.
- volume_filled = 850. remaining = 150 < 200.
Expected outcome:
- Intent transitions to FILLED (not PARTIALLY_FILLED).
- MarketModule.closeIntent() called.
- The 150-unit shortfall accepted as rounding residual.
- No re-posting or further matching.
Category: Partial Fill
T7.4: Partial fill swap fails, volume restored
Test name: should restore full available volume when partial fill swap fails
Preconditions:
- Intent: sell 1000 ALPHA, volume_filled=300 (from previous fill).
- Current deal negotiating for 400 units. Reservation of 400.
Steps:
- Swap fails (swap:failed event).
Expected outcome:
- volume_filled remains 300 (NOT updated because no fill occurred).
- Reservation of 400 released.
- getAvailable('ALPHA') restores the 400.
- Intent returns to ACTIVE (actually PARTIALLY_FILLED since volume_filled > 0).
- Available for matching = 1000 - 300 = 700.
Category: Partial Fill
Category 8: Volume Reservation
T8.1: Reserve volume decreases getAvailable()
Test name: should decrease getAvailable() when volume is reserved
Preconditions:
- Balance: 1000 ALPHA. No existing reservations.
Steps:
reserve('ALPHA', 400n, 'deal-1')-> returns true.- Check
getAvailable('ALPHA').
Expected outcome:
getAvailable('ALPHA') === 600n(1000 - 400).
Category: Volume Reservation
T8.2: Release reservation increases getAvailable()
Test name: should increase getAvailable() when reservation is released
Preconditions:
- Balance: 1000 ALPHA. Reservation: 400 for deal-1.
Steps:
release('deal-1').- Check
getAvailable('ALPHA').
Expected outcome:
getAvailable('ALPHA') === 1000n.getReservations()no longer contains deal-1.
Category: Volume Reservation
T8.3: Reserve more than available returns false
Test name: should return false when attempting to reserve more than available
Preconditions:
- Balance: 500 ALPHA. No reservations.
Steps:
reserve('ALPHA', 600n, 'deal-1').
Expected outcome:
- Returns
false. - No reservation created.
getAvailable('ALPHA') === 500n(unchanged).
Category: Volume Reservation
T8.4: Concurrent reservations (no over-commitment via mutex)
Test name: should serialize concurrent reserve() calls to prevent over-commitment
Preconditions:
- Balance: 1000 ALPHA. No reservations.
Steps:
- Fire two concurrent calls:
reserve('ALPHA', 700n, 'deal-1')andreserve('ALPHA', 700n, 'deal-2')simultaneously.
Expected outcome:
- Exactly one call returns
true, the other returnsfalse. - Total reservations <= 1000.
- Invariant:
sum(reservations['ALPHA']) <= getBalance('ALPHA'). - Mutex serialization prevents both from reading 1000 and both reserving 700.
Category: Volume Reservation
T8.5: External balance decrease causes negative available
Test name: should handle external balance decrease making getAvailable() go negative
Preconditions:
- Balance: 1000 ALPHA. Reservation: 800 for deal-1.
- External event reduces balance to 500 (e.g., owner withdrew via external wallet).
Steps:
- MockPaymentsModule.getBalance('ALPHA') now returns 500.
getAvailable('ALPHA')= 500 - 800 = -300.- Attempt
reserve('ALPHA', 100n, 'deal-2').
Expected outcome:
getAvailable('ALPHA')returns negative value (-300n or similar).- New
reserve()call returnsfalse(available < requested). - Existing reservation for deal-1 remains (cannot retroactively cancel).
- Warning logged about over-commitment state.
Category: Volume Reservation
Category 9: State Persistence
T9.1: Intent state persists across stop/restart
Test name: should preserve active intent state across agent stop and restart
Preconditions:
- Trader agent with MockFilesystem for persistence.
- Intent I1 created and in ACTIVE state.
Steps:
- Send
hm.stopto stop the trader container. - Send
hm.startto restart. - Send
LIST_INTENTScommand.
Expected outcome:
- Intent I1 appears in the list with state ACTIVE.
- Intent fields (direction, rates, volumes, etc.) match pre-stop values.
- TraderStateStore loaded from
/data/wallet/trader/intents/.
Category: State Persistence
T9.2: Mid-negotiation state persists across restart
Test name: should preserve deal state when stopped during NP-0 negotiation
Preconditions:
- Deal D1 in PROPOSED state between Trader A and Trader B.
Steps:
- Stop agent.
- Restart agent.
- Query deal state.
Expected outcome:
- Deal D1 restored from
/data/wallet/trader/deals/. - Deal state is PROPOSED (or timed out to CANCELLED if restart took > 30s).
- Volume reservation restored.
Category: State Persistence
T9.3: Mid-swap (EXECUTING) state recovered via SwapModule.load()
Test name: should recover in-flight swaps via SwapModule.load() on restart
Preconditions:
- Deal D1 in EXECUTING state with swap_id='swap-001'.
Steps:
- Stop agent.
- Restart agent. SwapModule.load() called during startup.
- MockSwapModule.load() restores swap-001.
Expected outcome:
- SwapModule.load() called during startup.
- In-flight swap resumed.
- Event listeners re-registered for swap:completed/failed/cancelled.
- Deal D1 state restored as EXECUTING.
Category: State Persistence
T9.4: Volume reservations persist across restart
Test name: should restore volume reservations from persisted state after restart
Preconditions:
- Reservation: 500 ALPHA for deal-1.
- VolumeReservationLedger serialized in
/data/wallet/trader/strategy.json.
Steps:
- Stop agent.
- Restart agent.
- Call
getAvailable('ALPHA')andgetReservations().
Expected outcome:
- Reservation for deal-1 (500 ALPHA) restored.
getAvailable('ALPHA')reflects the reservation.getReservations()returns[{ dealId: 'deal-1', coinId: 'ALPHA', amount: 500n }].
Category: State Persistence
T9.5: Strategy settings persist across restart
Test name: should restore SET_STRATEGY configuration after restart
Preconditions:
- Strategy set:
{ auto_match: true, auto_negotiate: true, max_concurrent_swaps: 5, min_search_score: 0.7 }.
Steps:
- Stop agent.
- Restart agent.
- Query strategy (via STATUS or GET_PORTFOLIO).
Expected outcome:
- All strategy fields restored from
/data/wallet/trader/strategy.json. auto_match: true,auto_negotiate: true,max_concurrent_swaps: 5,min_search_score: 0.7.
Category: State Persistence
Category 10: ACP Command Validation
T10.1: CREATE_INTENT with missing required fields
Test name: should return INVALID_PARAM when required fields are missing from CREATE_INTENT
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwith{ direction: 'sell' }(missing base_asset, quote_asset, rates, volumes, expiry_sec).
Expected outcome:
acp.errorresponse with error codeINVALID_PARAM.- Message indicates which field is missing.
- No intent created.
Category: ACP Command Validation
T10.2: CREATE_INTENT with negative rate
Test name: should reject CREATE_INTENT with negative rate_min or rate_max
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithrate_min: -10, rate_max: 500.
Expected outcome:
- Error:
INVALID_PARAM--rate_minmust be positive.
Category: ACP Command Validation
T10.3: CREATE_INTENT with zero volume
Test name: should reject CREATE_INTENT with zero volume_min or volume_max
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithvolume_min: 0, volume_max: 1000.
Expected outcome:
- Error:
INVALID_PARAM--volume_minmust be positive.
Category: ACP Command Validation
T10.4: CREATE_INTENT with already-expired timestamp
Test name: should reject CREATE_INTENT with expiry_sec that results in past expiry
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithexpiry_sec: 0orexpiry_sec: -100.
Expected outcome:
- Error:
INVALID_PARAM-- expiry must be in the future.
Category: ACP Command Validation
T10.5: CREATE_INTENT with rate_min > rate_max
Test name: should reject CREATE_INTENT when rate_min exceeds rate_max
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithrate_min: 600, rate_max: 400.
Expected outcome:
- Error:
INVALID_PARAM--rate_minmust be <=rate_max.
Category: ACP Command Validation
T10.6: CREATE_INTENT with same base_asset and quote_asset
Test name: should reject CREATE_INTENT when base_asset equals quote_asset
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithbase_asset: 'ALPHA', quote_asset: 'ALPHA'.
Expected outcome:
- Error:
INVALID_PARAM-- base_asset and quote_asset must differ.
Category: ACP Command Validation
T10.7: CREATE_INTENT with invalid asset name format
Test name: should reject CREATE_INTENT with asset name not matching /^[A-Z0-9_]{1,32}$/
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithbase_asset: 'alpha'(lowercase). - Send
CREATE_INTENTwithbase_asset: 'A'.repeat(33)(too long). - Send
CREATE_INTENTwithbase_asset: 'AL-PHA'(hyphen not allowed).
Expected outcome:
- All three return
INVALID_PARAMwith message about asset name format.
Category: ACP Command Validation
T10.8: CREATE_INTENT when max_active_intents reached
Test name: should return MAX_INTENTS_REACHED when intent cap is hit
Preconditions:
- Strategy:
max_active_intents: 3. - Already 3 active intents.
Steps:
- Send
CREATE_INTENTfor a 4th intent.
Expected outcome:
- Error:
MAX_INTENTS_REACHED. - No new intent created.
Category: ACP Command Validation
T10.9: CREATE_INTENT with insufficient balance
Test name: should return INSUFFICIENT_BALANCE when agent lacks tokens for sell intent
Preconditions:
- Balance: 100 ALPHA. No reservations.
Steps:
- Send
CREATE_INTENTwithdirection: 'sell', volume_max: 500.
Expected outcome:
- Error:
INSUFFICIENT_BALANCE. - No intent created.
Category: ACP Command Validation
T10.10: CANCEL_INTENT for non-existent intent
Test name: should return INTENT_NOT_FOUND for unknown intent_id
Preconditions:
- Trader agent spawned.
Steps:
- Send
CANCEL_INTENTwithintent_id: 'nonexistent0000...'(64 hex chars).
Expected outcome:
- Error:
INTENT_NOT_FOUND.
Category: ACP Command Validation
T10.11: CANCEL_INTENT for already-filled intent
Test name: should return INTENT_NOT_ACTIVE for filled intent
Preconditions:
- Intent I1 in FILLED state.
Steps:
- Send
CANCEL_INTENTwithintent_id: I1.intent_id.
Expected outcome:
- Error:
INTENT_NOT_ACTIVE-- intent is in terminal state.
Category: ACP Command Validation
T10.12: CANCEL_INTENT for intent with active deal
Test name: should return DEAL_IN_PROGRESS when intent has non-terminal deal
Preconditions:
- Intent I1 in NEGOTIATING state with active deal D1 (state PROPOSED).
Steps:
- Send
CANCEL_INTENTwithintent_id: I1.intent_id.
Expected outcome:
- Error:
DEAL_IN_PROGRESS-- cannot cancel intent while deal is active.
Category: ACP Command Validation
T10.13: SET_STRATEGY with invalid values
Test name: should reject SET_STRATEGY with out-of-range values
Preconditions:
- Trader agent spawned.
Steps:
- Send
SET_STRATEGYwithmax_concurrent_swaps: 11(max is 10). - Send
SET_STRATEGYwithmax_concurrent_swaps: 0(must be positive). - Send
SET_STRATEGYwithmin_search_score: 1.5(max is 1.0). - Send
SET_STRATEGYwithmin_search_score: -0.1(min is 0). - Send
SET_STRATEGYwithmax_active_intents: 101(max is 100).
Expected outcome:
- All return
INVALID_PARAMwith descriptive message. - Strategy unchanged.
Category: ACP Command Validation
T10.14: GET_PORTFOLIO returns correct available/reserved breakdown
Test name: should return accurate portfolio with available and reserved balances
Preconditions:
- Balance: 1000 ALPHA (confirmed: 900, unconfirmed: 100).
- Reservation: 300 ALPHA for deal-1, 200 ALPHA for deal-2.
Steps:
- Send
GET_PORTFOLIOcommand.
Expected outcome:
- Response contains:
balances:[{ asset: 'ALPHA', total: 1000, confirmed: 900, unconfirmed: 100, available: 500 }](1000 - 300 - 200 = 500 available).reserved:[{ asset: 'ALPHA', amount: 300, deal_id: 'deal-1' }, { asset: 'ALPHA', amount: 200, deal_id: 'deal-2' }].agent_pubkeyandagent_addresspresent.
Category: ACP Command Validation
T10.15: WITHDRAW_TOKEN with amount exceeding available
Test name: should reject WITHDRAW_TOKEN when amount exceeds available (reserved volume blocks)
Preconditions:
- Balance: 1000 ALPHA. Reservation: 800 for deal-1. Available: 200.
Steps:
- Send
WITHDRAW_TOKENwith{ asset: 'ALPHA', amount: '500', to_address: OWNER_ADDRESS }.
Expected outcome:
- Error:
INSUFFICIENT_BALANCE-- only 200 available after reservations. - No transfer initiated.
Category: ACP Command Validation
T10.16: WITHDRAW_TOKEN succeeds within available balance
Test name: should successfully withdraw tokens within available balance
Preconditions:
- Balance: 1000 ALPHA. Reservation: 300 for deal-1. Available: 700.
Steps:
- Send
WITHDRAW_TOKENwith{ asset: 'ALPHA', amount: '500', to_address: OWNER_ADDRESS }.
Expected outcome:
- MockPaymentsModule.send() called with
{ coinId: 'ALPHA', amount: '500', to: OWNER_ADDRESS }. - Response:
{ asset: 'ALPHA', amount: '500', to_address: OWNER_ADDRESS, transfer_id: '...', remaining_balance: '500' }.
Category: ACP Command Validation
T10.17: STATUS returns trader-specific fields
Test name: should include trader-specific fields in STATUS response
Preconditions:
- Trader agent running. 3 active intents, 1 pending swap, strategy configured.
Steps:
- Send
STATUSACP command.
Expected outcome:
- Response includes standard fields (status: 'RUNNING', message_count, etc.).
- Response also includes trader-specific fields:
active_intents: 3,pending_swaps: 1,completed_swaps: <count>,strategysummary.
Category: ACP Command Validation
T10.18: CREATE_INTENT with deposit_timeout_sec out of range
Test name: should reject CREATE_INTENT with deposit_timeout_sec outside 30-300 range
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithdeposit_timeout_sec: 10(below 30). - Send
CREATE_INTENTwithdeposit_timeout_sec: 500(above 300).
Expected outcome:
- Both return
INVALID_PARAM-- deposit_timeout_sec must be between 30 and 300.
Category: ACP Command Validation
Category 11: Security Scenarios
T11.1: swap:completed with payoutVerified=false must NOT update volume_filled (Section 7.9.2)
Test name: CRITICAL: should NOT update volume_filled when payoutVerified is false
Preconditions:
- Deal D1 in EXECUTING state. Agreed volume = 500.
Steps:
- MockSwapModule emits
swap:completedwith{ swapId: 'swap-001', payoutVerified: false }.
Expected outcome:
- volume_filled is NOT updated (remains at previous value).
- Deal remains in EXECUTING state (not COMPLETED).
- Retry mechanism triggered: verifyPayout() called periodically (every 30s, max 10 retries).
- Reservation NOT released (deal still active).
Category: Security
T11.2: payoutVerified=false followed by successful retry
Test name: should complete deal after payoutVerified succeeds on retry
Preconditions:
- Deal D1 in EXECUTING state. First swap:completed had payoutVerified=false.
vi.useFakeTimers().
Steps:
- Receive swap:completed with payoutVerified=false.
- verifyPayout() returns false on first 3 retries.
- Advance time by 30s intervals.
- verifyPayout() returns true on retry 4.
Expected outcome:
- Deal transitions to COMPLETED after successful verification.
- volume_filled updated.
- Reservation released.
Category: Security
T11.3: payoutVerified=false exhausts all retries
Test name: should escalate to owner and FAIL deal after max payout verification retries
Preconditions:
- Deal D1 in EXECUTING state.
vi.useFakeTimers().
Steps:
- Receive swap:completed with payoutVerified=false.
- verifyPayout() returns false on all 10 retries.
- Advance time by 300+ seconds.
Expected outcome:
- Deal transitions to FAILED after 10th retry fails.
- ACP status message sent to owner about payout verification failure.
- volume_filled NOT updated.
- Reservation released.
Category: Security
T11.4: Counterparty proposes SwapDeal with different terms than NP-0 (Section 7.9.4)
Test name: CRITICAL: should reject swap proposal with terms diverging from NP-0 agreement
Preconditions:
- Deal D1 agreed via NP-0: rate=475, volume=300, base_asset='ALPHA', quote_asset='USDC'.
- Counterparty is the acceptor.
Steps:
- Trader receives
swap:proposal_receivedevent. - SwapDeal in the event has
partyAAmount: '500'(instead of expected '300'). - SwapExecutor compares received SwapDeal fields against stored DealTerms.
Expected outcome:
- SwapExecutor detects term mismatch: partyAAmount '500' != expected '300'.
- Swap rejected via rejectSwap().
- Deal transitions to FAILED with reason indicating term mismatch.
- Security event logged.
- Volume reservation released.
Category: Security
T11.5: Reject v1 protocol proposal (Section 7.9.5)
Test name: CRITICAL: should reject swap proposal with protocolVersion !== 2
Preconditions:
- Deal in ACCEPTED state. Awaiting swap execution.
Steps:
- Trader receives
swap:proposal_receivedwithprotocolVersion: 1.
Expected outcome:
- SwapExecutor checks protocolVersion.
- Calls rejectSwap() for the v1 proposal.
- Deal transitions to FAILED.
- Security warning logged: v1 protocol lacks mutual consent and is vulnerable to MITM.
Category: Security
T11.6: Concurrent reserve() calls (no over-commitment) (Section 7.9.6)
Test name: CRITICAL: should prevent over-commitment when concurrent matches try to reserve simultaneously
Preconditions:
- Balance: 1000 ALPHA. No reservations.
- Two match evaluations run concurrently, each trying to reserve 800.
Steps:
- Concurrently call
reserve('ALPHA', 800n, 'deal-A')andreserve('ALPHA', 800n, 'deal-B').
Expected outcome:
- Exactly one succeeds (returns true), one fails (returns false).
- Total reserved <= 1000.
- Async mutex ensures serialization.
sum(reservations['ALPHA']) <= PaymentsModule.getBalance('ALPHA').
Category: Security
T11.7: SIGTERM during active swap triggers graceful shutdown (Section 7.9.8)
Test name: should persist all state on SIGTERM during active swap
Preconditions:
- Deal D1 in EXECUTING state. Intent I1 in NEGOTIATING state. Reservation active.
- MockFilesystem for persistence.
Steps:
- Send
hm.stopto container (which sends SHUTDOWN_GRACEFUL ACP command, then SIGTERM). - Agent processes shutdown: persists TraderStateStore, VolumeReservationLedger.
Expected outcome:
- All state persisted to
/data/wallet/trader/. - Deals, intents, strategy, reservations written atomically.
- On restart, all state recovered.
- SwapModule.load() recovers in-flight swap.
Category: Security
T11.8: NP-0 message replay detection
Test name: should reject replayed NP-0 message with duplicate msg_id
Preconditions:
- Agent maintains deduplication window of 600s / 10000 entries.
- Previous message received with
msg_id: 'uuid-1'.
Steps:
- Resend identical NP-0 message with same
msg_id: 'uuid-1'.
Expected outcome:
- Second message rejected as duplicate.
- No state change.
- Replay attempt logged.
Category: Security
T11.9: NP-0 message with stale timestamp (clock skew > 300s)
Test name: should reject NP-0 message with ts_ms older than 300s
Preconditions:
- Current time = T.
Steps:
- Receive NP-0 message with
ts_ms = T - 301000(301 seconds old).
Expected outcome:
- Message rejected due to clock skew tolerance exceeded (300s / 300000ms).
- No state change.
Category: Security
T11.10: Intent signature verification on search results
Test name: should verify ECDSA signature on discovered intents before matching
Preconditions:
- Search result contains intent with valid fields but corrupted signature.
Steps:
- IntentEngine parses search result.
- Recomputes intent_id from canonical JSON of extracted fields.
- Verifies ECDSA signature against agentPublicKey.
- Signature verification fails.
Expected outcome:
- Intent discarded from match candidates.
- Warning logged: signature verification failed.
- No negotiation initiated.
Category: Security
T11.11: Deposit idempotency check on crash recovery (Section 7.9.3)
Test name: should check escrow status before re-depositing on crash recovery
Preconditions:
- Deal D1 in EXECUTING state.
deposit_attempted: truepersisted.localDepositTransferIdmissing.
Steps:
- Agent restarts. SwapModule.load() recovers swap.
- Agent checks
deposit_attemptedflag. - Queries escrow via
getSwapStatus({ queryEscrow: true }). - Escrow confirms deposit was received.
Expected outcome:
- Agent does NOT re-deposit (avoids double payment).
- Swap continues from post-deposit state.
- If escrow confirms NOT received, then re-deposit proceeds.
Category: Security
Category 12: MarketModule Integration
T12.1: postIntent() failure (API down)
Test name: should keep intent in DRAFT state when MarketModule.postIntent() fails
Preconditions:
- MockMarketModule.postIntent() rejects with network error.
Steps:
- Send
CREATE_INTENTcommand.
Expected outcome:
acp.errorreturned to owner with appropriate error.- Intent not persisted as ACTIVE (stays in DRAFT or discarded).
- No market_intent_id assigned.
Category: MarketModule Integration
T12.2: search() returns no results
Test name: should handle empty search results gracefully
Preconditions:
- Active intent exists. MockMarketModule.search() returns empty array.
Steps:
- IntentEngine scan loop fires.
- search() returns
[].
Expected outcome:
- No match initiated.
- Intent remains ACTIVE.
- No errors logged (empty results are normal).
Category: MarketModule Integration
T12.3: search() results with score below minScore filtered
Test name: should filter search results below min_search_score threshold
Preconditions:
- Strategy:
min_search_score: 0.7. - search() returns results with scores: [0.9, 0.75, 0.65, 0.5].
Steps:
- IntentEngine processes results.
Expected outcome:
- Only results with score >= 0.7 considered: [0.9, 0.75].
- Results with score 0.65 and 0.5 filtered out.
- Note: minScore is passed to search() as server-side filter, but client-side re-check is a defense-in-depth measure.
Category: MarketModule Integration
T12.4: subscribeFeed() disconnects then falls back to periodic search()
Test name: should fall back to periodic search() when WebSocket feed disconnects
Preconditions:
- subscribeFeed() initially connected.
Steps:
- subscribeFeed() onError callback fires (WebSocket disconnection).
- IntentEngine continues to run periodic search() scan loop.
Expected outcome:
- Feed disconnection logged as warning.
- Periodic search() continues at configured interval (5s).
- Matching still works via search()-based discovery.
- Feed reconnection attempted (if supported by SDK).
Category: MarketModule Integration
T12.5: closeIntent() failure
Test name: should log warning but not crash when MarketModule.closeIntent() fails
Preconditions:
- Intent fully filled, needs to be closed.
- MockMarketModule.closeIntent() rejects.
Steps:
- Intent transitions to FILLED.
- closeIntent() called but fails.
Expected outcome:
- Warning logged about closeIntent() failure.
- Intent state still transitions to FILLED locally.
- Agent does not crash.
- Stale intent may remain in search index (acceptable: it will expire server-side).
Category: MarketModule Integration
T12.6: getMyIntents() on startup reconciles local state
Test name: should reconcile local intent state against MarketModule on startup
Preconditions:
- Local state has 3 intents: I1 (ACTIVE), I2 (ACTIVE), I3 (PARTIALLY_FILLED).
- MockMarketModule.getMyIntents() returns only I1 and I3 (I2 was closed server-side).
Steps:
- Agent starts up. TraderStateStore loaded.
- getMyIntents() called for reconciliation.
Expected outcome:
- I1: remains ACTIVE (present in both local and server).
- I2: transitions to CANCELLED or EXPIRED (present locally but not on server -- was closed externally).
- I3: remains PARTIALLY_FILLED (present in both).
- Reconciliation logged.
Category: MarketModule Integration
Category 13: Multi-Agent Scenarios
T13.1: Two trader agents discover each other and complete a swap
Test name: should complete full trading flow between two trader agents
Preconditions:
- Trader A spawned with 1000 ALPHA. Posts sell intent: ALPHA/USDC, rate 450-500, volume 500.
- Trader B spawned with 250000 USDC. Posts buy intent: ALPHA/USDC, rate 460-490, volume 300.
- Both connected to same MockMarketModule and MockSphereNetwork.
Steps:
- A's scan discovers B's buy intent. Match found.
- A (lower pubkey) proposes deal: rate=475, volume=300.
- B receives proposal, validates, sends accept.
- A pings escrow (success), proposes swap.
- B receives swap proposal, validates terms match NP-0, accepts.
- Both deposit to escrow.
- Escrow confirms, releases tokens.
- swap:completed emitted to both.
Expected outcome:
- Both deals in COMPLETED state.
- A: volume_filled=300, intent PARTIALLY_FILLED (had 500 offered).
- B: volume_filled=300, intent FILLED (had 300 offered).
- Reservations released on both sides.
- B's intent closed on MarketModule (fully filled).
- A's intent re-posted with remaining 200.
Category: Multi-Agent
T13.2: Three agents, two match same counterparty (deterministic selection)
Test name: should allow only one of two competing agents to propose against same intent
Preconditions:
- Trader A posts sell intent.
- Trader B and Trader C both discover A's intent simultaneously.
- PK_B < PK_C (B has lower pubkey).
Steps:
- B evaluates match with A: B's pubkey < A's pubkey? Determines proposer role.
- C evaluates match with A: C's pubkey vs A's pubkey.
- Both attempt to initiate negotiation with A.
Expected outcome:
- Whichever agent has lower pubkey than A becomes proposer.
- If both B and C propose, A will accept the first and reject the second with AGENT_BUSY.
- Only one deal proceeds to completion for intent A.
- The rejected agent releases its reservation and can try matching again.
Category: Multi-Agent
T13.3: Simultaneous complementary intents
Test name: should resolve race when A and B post complementary intents simultaneously
Preconditions:
- Trader A posts sell ALPHA/USDC at same time Trader B posts buy ALPHA/USDC.
- Both agents' scan loops detect the other's intent.
Steps:
- Both IntentEngines find a match in the same scan cycle.
- Proposer selection: agent with lower pubkey proposes.
- Higher-pubkey agent waits for incoming proposal.
Expected outcome:
- Exactly one np.propose_deal sent (from lower pubkey agent).
- Higher pubkey agent receives and processes the proposal.
- Normal negotiation and swap flow follows.
- No duplicate deals created.
Category: Multi-Agent
T13.4: Agent cannot trade with itself (same pubkey filtered)
Test name: should prevent agent from matching its own intents (same pubkey)
Preconditions:
- Trader A has both a buy and sell intent for ALPHA/USDC with overlapping rates.
- Both intents discoverable via search().
Steps:
- IntentEngine scans for matches.
- search() returns A's own sell intent when scanning for A's buy intent matches.
Expected outcome:
- Criterion 8 (different agents) filters out the result.
agent_pubkey === own_pubkey-> skip.- No self-negotiation attempted.
Category: Multi-Agent
Category 14: Edge Cases
TA.1: CREATE_INTENT for buy intent does not check sell-side balance
Test name: should not require base_asset balance for buy intents
Preconditions:
- Balance: 50000 USDC, 0 ALPHA.
Steps:
- Send
CREATE_INTENTwithdirection: 'buy', base_asset: 'ALPHA', quote_asset: 'USDC', volume_max: 100, rate_max: 500.
Expected outcome:
- Intent created successfully. Buy intents require quote_asset balance (USDC), not base_asset.
- Agent needs USDC to pay, not ALPHA.
- Balance check: 100 * 500 = 50000 USDC needed. Balance is 50000. Passes.
Category: Edge Case
TA.2: NP-0 message with NaN or Infinity in numeric fields
Test name: should reject NP-0 message with non-finite numeric values
Preconditions:
- Deal in negotiation.
Steps:
- Receive np.propose_deal with
terms.rate: NaN. - Receive np.propose_deal with
terms.volume: Infinity.
Expected outcome:
- Both rejected during DealTerms validation (rate and volume must be positive finite).
Category: Edge Case
TA.3: Intent with volume_min === volume_max (exact amount only)
Test name: should support intents where volume_min equals volume_max
Preconditions:
- Trader agent spawned.
Steps:
- Send
CREATE_INTENTwithvolume_min: 500, volume_max: 500.
Expected outcome:
- Intent created. Only exact 500-unit fills accepted.
- Partial fills below 500 would fail criterion 4.
Category: Edge Case
TA.4: Rapid intent create/cancel/create cycle
Test name: should handle rapid intent lifecycle transitions without state corruption
Preconditions:
- Trader agent spawned.
Steps:
- CREATE_INTENT -> get intent_id I1.
- CANCEL_INTENT I1.
- CREATE_INTENT (same params) -> get intent_id I2.
- LIST_INTENTS.
Expected outcome:
- I1 in CANCELLED state.
- I2 in ACTIVE state with different intent_id (different salt).
- Two intents in list.
- MarketModule: postIntent called twice, closeIntent called once.
Category: Edge Case
TA.5: LIST_INTENTS and LIST_SWAPS pagination boundary
Test name: should respect limit and offset pagination boundaries
Preconditions:
- 5 intents created.
Steps:
LIST_INTENTS { limit: 3, offset: 0 }-> 3 results, total: 5.LIST_INTENTS { limit: 3, offset: 3 }-> 2 results, total: 5.LIST_INTENTS { limit: 3, offset: 5 }-> 0 results, total: 5.LIST_INTENTS { limit: 201 }-> error (max 200).
Expected outcome:
- Pagination works correctly.
- Offset beyond total returns empty array.
- Limit > 200 returns INVALID_PARAM.
Category: Edge Case
TA.6: Escrow address validation
Test name: should validate escrow_address as 'any' or valid secp256k1 compressed pubkey
Preconditions:
- Trader agent spawned.
Steps:
CREATE_INTENTwithescrow_address: 'any'-> success.CREATE_INTENTwithescrow_address: '02' + 'a'.repeat(64)-> success (valid format).CREATE_INTENTwithescrow_address: 'invalid'-> error.CREATE_INTENTwithescrow_address: '04' + 'a'.repeat(128)-> error (uncompressed not accepted).
Expected outcome:
- Cases 1-2 succeed.
- Cases 3-4 return INVALID_PARAM.
Category: Edge Case
TA.7: Blocked counterparty filtering
Test name: should filter out intents from blocked counterparties during matching
Preconditions:
- Strategy:
blocked_counterparties: [PK_BLOCKED]. - search() returns intent from PK_BLOCKED agent.
Steps:
- IntentEngine processes search results.
Expected outcome:
- Intent from PK_BLOCKED filtered out.
- No match initiated with blocked counterparty.
Category: Edge Case
TA.8: Trusted escrow filtering during matching
Test name: should reject matches proposing untrusted escrow when trusted_escrows is configured
Preconditions:
- Strategy:
trusted_escrows: [ESCROW_PK_1]. - Counterparty intent has
escrow_address: ESCROW_PK_2(not in trusted list).
Steps:
- Evaluate match. Escrow compatibility check.
Expected outcome:
- Match fails escrow compatibility (criterion 7 extended by strategy).
- If counterparty says "any" and we have trusted_escrows, we use our preferred escrow.
- If counterparty specifies an untrusted escrow, match is rejected.
Category: Edge Case
TA.9: Intent expiry during active negotiation
Test name: should handle intent expiring while deal is in progress
Preconditions:
- Intent I1 with expiry_ms approaching. Deal D1 in PROPOSED state.
vi.useFakeTimers().
Steps:
- Advance time past expiry_ms.
- Expiry sweep runs.
Expected outcome:
- If deal is in non-terminal state, intent should not transition to EXPIRED mid-deal.
- The deal's own timeouts handle cancellation.
- Intent transitions to EXPIRED only after deal reaches terminal state (or EXPIRED takes priority per implementation).
- Implementation note: verify which takes precedence -- deal timeout or intent expiry.
Category: Edge Case
TA.10: Description parsing of non-standard format
Test name: should skip intents with unparseable description format
Preconditions:
- search() returns result with description: "Random text that does not match canonical format".
Steps:
- IntentEngine attempts to parse description.
- Parser fails to extract rate/volume/escrow fields.
Expected outcome:
- Result skipped (per spec: "Unrecognized description formats SHOULD be skipped").
- No error thrown.
- Matching continues with remaining results.
Category: Edge Case
Category 15: Adversarial Counterparty Attacks
T15.1: Forged NP-0 signature from known counterparty
Test name: should reject NP-0 message with forged ECDSA signature even when sender_pubkey is valid participant
Preconditions:
- Deal in PROPOSED state between PK_A (proposer) and PK_B (acceptor).
- Attacker has PK_B's pubkey but not private key.
Steps:
- Construct
np.accept_dealwith correctsender_pubkey: PK_Band validdeal_id, but sign with a different private key (forged signature). - Deliver message to Trader A.
Expected outcome:
- ECDSA signature verification fails against PK_B.
- Message rejected silently (logged as warning).
- Deal state remains PROPOSED.
- No state transition occurs.
Category: Adversarial Counterparty
Note: Extends T4.4 (which tests corrupted signatures). This test specifically validates that a valid participant pubkey with a forged signature is caught.
T15.2: NP-0 messages after deal reaches terminal state
Test name: should ignore NP-0 messages referencing a deal in terminal state
Preconditions:
- Deal D1 between PK_A and PK_B completed (state COMPLETED).
- Valid signatures from PK_B.
Steps:
- Send
np.accept_dealwithdeal_id = D1.deal_idand valid signature. - Send
np.reject_dealwithdeal_id = D1.deal_idand valid signature. - Send
np.propose_dealwithdeal_id = D1.deal_idand valid signature.
Expected outcome:
- All three messages ignored.
- Deal state remains COMPLETED.
- Warning logged: "NP-0 message for terminal deal".
- No volume reservation changes.
Category: Adversarial Counterparty
T15.3: Proposal flood to exhaust max_concurrent_swaps
Test name: should reject proposals beyond max_concurrent_swaps limit
Preconditions:
- Strategy:
max_concurrent_swaps: 3. - Trader B has 5 active intents matching Trader A's intents.
- 3 deals already in non-terminal state (PROPOSED, ACCEPTED, or EXECUTING).
Steps:
- Trader C sends
np.propose_dealfor Trader A's 4th intent. - Trader D sends
np.propose_dealfor Trader A's 5th intent.
Expected outcome:
- Both proposals rejected with
reason_code: 'AGENT_BUSY'. - No new deals created beyond the limit of 3.
- Existing deals unaffected.
- Rejection logged with rate-limit context.
Category: Adversarial Counterparty
T15.4: Wrong currency deposit bounced by escrow
Test name: should detect wrong currency in escrow deposit and fail deal
Preconditions:
- Deal agreed: sell 300 ALPHA for USDC. Escrow expects ALPHA deposit from seller.
- MockSwapModule emits
swap:failedwith error indicating currency mismatch from escrow.
Steps:
- SwapExecutor proceeds with deposit.
- Escrow rejects deposit due to wrong currency.
swap:failedevent emitted with error details.
Expected outcome:
- Deal transitions EXECUTING -> FAILED with reason indicating currency mismatch.
- Volume reservation released.
- Intent returns to ACTIVE.
- Error logged with currency details for debugging.
Category: Adversarial Counterparty
T15.5: Forged announce_result from non-escrow sender
Test name: should reject announce_result message from sender that is not the designated escrow
Preconditions:
- Deal in EXECUTING state with
escrow_address: ESCROW_PK. - Attacker sends a fake
announce_resultfromPK_ATTACKER(not ESCROW_PK).
Steps:
- MockSwapModule receives announce_result from PK_ATTACKER.
- SwapModule verifies sender matches expected escrow_address.
Expected outcome:
- Message rejected: sender is not the designated escrow.
- Deal state unchanged (remains EXECUTING).
- Security event logged: "announce_result from non-escrow sender".
- No payout processed.
Category: Adversarial Counterparty
T15.6: Deposit invoice amount mismatch detected
Test name: should detect and reject escrow deposit invoice with mismatched amount
Preconditions:
- Deal agreed: sell 300 ALPHA at rate 475. Expected deposit: 300 ALPHA.
- Escrow returns invoice requesting 350 ALPHA (inflated amount).
Steps:
- SwapExecutor receives deposit invoice from escrow.
- Compares invoice amount against agreed DealTerms.
Expected outcome:
- Invoice amount 350 does not match expected 300.
- Deposit not initiated.
- Deal transitions to FAILED with reason: "deposit invoice amount mismatch".
- Volume reservation released.
- Security warning logged.
Category: Adversarial Counterparty
T15.7: Escrow payout wrong currency caught by verifyPayout
Test name: should catch escrow payout in wrong currency via verifyPayout()
Preconditions:
- Deal completed: Trader A sold 300 ALPHA, expects USDC payout.
- MockSwapModule emits
swap:completedwithpayoutVerified: false. - verifyPayout() detects payout currency is ALPHA (not expected USDC).
Steps:
- Receive
swap:completedwithpayoutVerified: false. - verifyPayout() inspects payout transaction details.
- Currency mismatch detected.
Expected outcome:
- volume_filled NOT updated.
- verifyPayout() retries fail (currency remains wrong).
- After max retries, deal transitions to FAILED.
- Owner notified via ACP status message about payout currency mismatch.
- Reservation released.
Category: Adversarial Counterparty
T15.8: Escrow payout insufficient amount caught by verifyPayout
Test name: should catch escrow payout with insufficient amount via verifyPayout()
Preconditions:
- Deal completed: Trader A expects 142500 USDC (300 * 475).
- MockSwapModule emits
swap:completedwithpayoutVerified: false. - verifyPayout() detects payout amount is only 100000 USDC (underpayment).
Steps:
- Receive
swap:completedwithpayoutVerified: false. - verifyPayout() inspects payout amount.
- Amount insufficient: 100000 < expected 142500.
Expected outcome:
- volume_filled NOT updated.
- verifyPayout() retries fail (amount remains insufficient).
- After max retries, deal transitions to FAILED.
- Owner notified about payout amount discrepancy.
- Reservation released.
Category: Adversarial Counterparty
Category 16: State Machine Violation Tests
T16.1: FILLED to ACTIVE transition rejected
Test name: should reject state transition from FILLED to ACTIVE
Preconditions:
- Intent I1 in FILLED state (volume_filled === volume_max).
Steps:
- Attempt to transition I1 from FILLED to ACTIVE (e.g., via internal state machine call or by receiving a match event for the filled intent).
- Verify state machine guard rejects the transition.
Expected outcome:
- Transition rejected. Intent remains in FILLED state.
- Error logged: "invalid state transition FILLED -> ACTIVE".
- No volume reservation created.
- No market intent re-posted.
Category: State Machine Violation
T16.2: CANCELLED to any transition rejected
Test name: should reject all state transitions from CANCELLED state
Preconditions:
- Intent I1 in CANCELLED state.
Steps:
- Attempt transition CANCELLED -> ACTIVE (via re-activation).
- Attempt transition CANCELLED -> MATCHING (via match event).
- Attempt transition CANCELLED -> NEGOTIATING (via incoming proposal).
- Attempt transition CANCELLED -> FILLED (via swap completion event).
Expected outcome:
- All four transitions rejected.
- Intent remains in CANCELLED state for each attempt.
- Error logged for each invalid transition attempt.
- No side effects (no reservations, no NP-0 messages, no MarketModule calls).
Category: State Machine Violation
T16.3: Double completion of same deal prevented
Test name: should prevent double completion of the same deal
Preconditions:
- Deal D1 in EXECUTING state. Agreed volume = 300.
- Intent volume_filled = 200.
Steps:
- MockSwapModule emits
swap:completedwithpayoutVerified: truefor D1. - Deal transitions to COMPLETED. volume_filled = 500.
- MockSwapModule emits a second
swap:completedwithpayoutVerified: truefor same D1 (duplicate event).
Expected outcome:
- First completion processed normally: volume_filled = 500, deal COMPLETED.
- Second completion ignored: deal already in terminal state COMPLETED.
- volume_filled remains 500 (not 800).
- Warning logged: "duplicate swap:completed for terminal deal".
Category: State Machine Violation
T16.4: Negotiation on expired intent rejected
Test name: should reject incoming NP-0 proposal for an expired intent
Preconditions:
- Intent I1 created with
expiry_sec: 60. Intent has transitioned to EXPIRED. - Trader B sends
np.propose_dealreferencing I1.
Steps:
- Receive
np.propose_dealwithacceptor_intent_idmatching I1. - Agent checks intent state.
Expected outcome:
- Proposal rejected with
np.reject_dealand reason_codeOTHER(or implementation-specific reason indicating intent expired). - No deal created.
- No volume reserved.
- Rejection logged with context: "proposal for expired intent".
Category: State Machine Violation
Category 17: Unicity SDK Integration
T17.1: Balance change from external deposit during active reservation
Test name: should correctly reflect external deposit in available balance while reservations are active
Preconditions:
- Balance: 1000 ALPHA. Reservation: 600 for deal-1. Available: 400.
- External deposit of 500 ALPHA arrives (balance increases to 1500).
Steps:
- MockPaymentsModule.getBalance('ALPHA') now returns 1500.
- Check
getAvailable('ALPHA'). - Attempt
reserve('ALPHA', 800n, 'deal-2').
Expected outcome:
getAvailable('ALPHA')= 1500 - 600 = 900.- New reservation of 800 succeeds (800 <= 900).
- Total reservations: 600 + 800 = 1400 <= 1500.
- No warning logged (balance increase is healthy).
Category: Unicity SDK Integration
Note: Extends T8.5 (which tests balance decrease). This tests the positive case of external deposits.
T17.2: Token invalidated by L3 decreases available volume
Test name: should decrease available volume when token is invalidated by L3 aggregator
Preconditions:
- Balance: 1000 ALPHA (composed of 3 tokens: 400 + 300 + 300).
- L3 aggregator invalidates the 400 token (inclusion proof revoked).
- MockPaymentsModule.getBalance('ALPHA') drops to 600.
Steps:
- Balance callback fires with new balance 600.
- Existing reservation of 500 for deal-1.
- Check
getAvailable('ALPHA').
Expected outcome:
getAvailable('ALPHA')= 600 - 500 = 100.- Warning logged: "balance decreased due to token invalidation".
- New reservations limited to 100 available.
- Existing deal-1 reservation remains (cannot retroactively cancel).
Category: Unicity SDK Integration
T17.3: Token split succeeds during deposit and swap completes
Test name: should complete swap when token split is required for deposit amount
Preconditions:
- Trader A holds single 1000 ALPHA token. Deal requires deposit of 300 ALPHA.
- MockPaymentsModule.send() internally splits the 1000 token into 300 + 700.
Steps:
- SwapExecutor initiates deposit of 300 ALPHA to escrow.
- PaymentsModule performs token split via state-transition-sdk.
- Split succeeds. 300 token deposited to escrow.
- Swap completes normally.
Expected outcome:
- Token split transparent to SwapExecutor.
- Deposit succeeds with correct amount.
- swap:completed emitted with payoutVerified: true.
- Remaining balance: 700 ALPHA + payout.
Category: Unicity SDK Integration
T17.4: Token split fails (aggregator unreachable) and swap times out
Test name: should time out swap when token split fails due to unreachable aggregator
Preconditions:
- Trader A holds single 1000 ALPHA token. Deal requires deposit of 300 ALPHA.
- MockPaymentsModule.send() fails because L3 aggregator is unreachable for split operation.
vi.useFakeTimers().
Steps:
- SwapExecutor initiates deposit.
- PaymentsModule.send() rejects with aggregator timeout error.
- Advance time past deposit_timeout_sec.
Expected outcome:
- Deposit fails. swap:failed event emitted.
- Deal transitions EXECUTING -> FAILED.
- Volume reservation released.
- Intent returns to ACTIVE for re-matching.
- Error logged: "token split failed: aggregator unreachable".
Category: Unicity SDK Integration
T17.5: Tokens in transferring status excluded from reservations
Test name: should exclude tokens with 'transferring' status from available balance for reservations
Preconditions:
- Balance: 1000 ALPHA total, but 300 ALPHA in 'transferring' status (pending L3 confirmation).
- MockPaymentsModule.getBalance('ALPHA') returns 700 (only confirmed tokens).
Steps:
- Check
getAvailable('ALPHA'). - Attempt
reserve('ALPHA', 800n, 'deal-1').
Expected outcome:
getAvailable('ALPHA')= 700 (transferring tokens excluded by PaymentsModule).- Reservation of 800 fails (800 > 700).
- Returns false.
- No over-commitment against unconfirmed tokens.
Category: Unicity SDK Integration
T17.6: SwapModule.load() recovers orphaned swaps with stale state
Test name: should recover orphaned swaps via SwapModule.load() and reconcile against escrow
Preconditions:
- Deal D1 was in EXECUTING state when agent crashed.
- Swap swap-001 has no local completion event recorded.
- Escrow has already completed the swap (both parties deposited, payouts sent).
Steps:
- Agent restarts. SwapModule.load() called.
- MockSwapModule.load() restores swap-001 in 'pending' state.
- Agent queries escrow status: escrow reports swap completed.
- verifyPayout() confirms payout received.
Expected outcome:
- SwapModule.load() recovers the orphaned swap.
- Escrow query reveals completed status.
- verifyPayout() succeeds.
- Deal transitions to COMPLETED. volume_filled updated.
- Reservation released.
Category: Unicity SDK Integration
Note: Extends T9.3 (which tests basic load recovery). This tests recovery when escrow state has advanced beyond local state.
T17.7: maxPendingSwaps limit reached rejects new proposals
Test name: should reject new swap proposals when maxPendingSwaps limit is reached
Preconditions:
- Strategy:
max_concurrent_swaps: 3. - 3 swaps currently in EXECUTING state.
- New match found with Trader B.
Steps:
- IntentEngine detects match with Trader B.
- Checks concurrent swap count: 3 === max_concurrent_swaps.
- Match evaluation skipped.
Expected outcome:
- No
np.propose_dealsent to Trader B. - Match deferred until a current swap completes.
- Intent remains ACTIVE (not transitioned to MATCHING).
- Debug log: "max_concurrent_swaps reached, deferring match".
Category: Unicity SDK Integration
T17.8: Nametag resolution failure causes swap proposal failure
Test name: should fail swap proposal when nametag resolution fails for escrow address
Preconditions:
- Deal agreed with
escrow_addressspecified as a nametag (human-readable identifier). - Nametag resolution via Nostr relay fails (relay unreachable or nametag not found).
Steps:
- SwapExecutor attempts to resolve escrow nametag to secp256k1 pubkey.
- Resolution fails with timeout or not-found error.
- pingEscrow() cannot proceed without resolved address.
Expected outcome:
- Deal transitions ACCEPTED -> FAILED with reason: "escrow address resolution failed".
- Volume reservation released.
- Intent returns to ACTIVE.
- Error logged with nametag and resolution failure details.
Category: Unicity SDK Integration
T17.9: Consent signature verification on acceptor side
Test name: should verify consent signature in swap proposal matches NP-0 agreed terms
Preconditions:
- Deal D1 agreed via NP-0 between PK_A (proposer) and PK_B (acceptor).
- PK_A sends swap proposal via SwapModule with consent signature.
Steps:
- PK_B receives
swap:proposal_receivedevent. - SwapExecutor extracts consent signature from SwapDeal.
- Verifies signature covers the canonical DealTerms (rate, volume, escrow, parties).
- Signature is valid and matches PK_A.
Expected outcome:
- Consent signature verified successfully.
- Swap accepted (acceptSwap() called).
- Deal proceeds to deposit phase.
Category: Unicity SDK Integration
T17.10: Consent signature verification fails on acceptor side
Test name: should reject swap proposal when consent signature verification fails
Preconditions:
- Deal D1 agreed via NP-0. PK_A sends swap proposal with invalid consent signature.
Steps:
- PK_B receives
swap:proposal_receivedevent. - SwapExecutor verifies consent signature.
- Signature does not match expected DealTerms or PK_A pubkey.
Expected outcome:
- Consent signature verification fails.
- Swap rejected via rejectSwap().
- Deal transitions to FAILED with reason: "consent signature verification failed".
- Security event logged.
- Volume reservation released.
Category: Unicity SDK Integration
T17.11: L2 BFT consensus delay does not cause false timeout
Test name: should not falsely time out swap when L2 BFT consensus is delayed
Preconditions:
- Deal in EXECUTING state. Deposit sent.
- L2 BFT consensus delayed (block confirmation takes longer than usual but within tolerance).
vi.useFakeTimers().- deposit_timeout_sec = 300.
Steps:
- Deposit transaction submitted.
- L2 consensus delayed by 60 seconds (normal round is ~1 second).
- Advance time by 60 seconds.
- Consensus completes. Deposit confirmed.
- Swap proceeds normally.
Expected outcome:
- Swap NOT timed out (60s delay is well within 300s deposit_timeout_sec).
- Deposit confirmed after consensus delay.
- swap:completed emitted normally.
- No false failure due to consensus latency.
Category: Unicity SDK Integration
T17.12: Stale L3 inclusion proof triggers retry verification
Test name: should retry payout verification when L3 inclusion proof is stale
Preconditions:
- Deal completed. verifyPayout() called.
- First verification attempt returns stale inclusion proof (proof references old SMT root).
vi.useFakeTimers().
Steps:
- verifyPayout() called. Returns false due to stale proof.
- Advance time by 30 seconds.
- verifyPayout() called again. L3 aggregator returns fresh inclusion proof.
- Verification succeeds.
Expected outcome:
- First attempt: payoutVerified = false (stale proof).
- Second attempt: payoutVerified = true (fresh proof).
- Deal transitions to COMPLETED.
- volume_filled updated.
- Retry mechanism handles transient L3 staleness gracefully.
Category: Unicity SDK Integration
T17.13: Agent pubkey matches posted intent contactHandle
Test name: should ensure agent pubkey matches contactHandle in posted market intent
Preconditions:
- Trader agent spawned with known pubkey PK_AGENT.
Steps:
- Send
CREATE_INTENTwith valid params. - Inspect MockMarketModule.postIntent() call arguments.
Expected outcome:
contactHandlein postIntent() call matches PK_AGENT's Sphere address.- Counterparties can reach the agent via the contactHandle.
- contactHandle is deterministically derived from the agent's secp256k1 identity.
Category: Unicity SDK Integration
T17.14: HD-derived address consistency in intents
Test name: should produce consistent HD-derived addresses across agent restarts
Preconditions:
- Agent uses BIP-32 HD key derivation from stored mnemonic.
- Agent restarted with same mnemonic.
Steps:
- Record agent pubkey and Sphere address before restart.
- Stop and restart agent (same wallet, same mnemonic).
- Create a new intent.
- Inspect contactHandle in postIntent() call.
Expected outcome:
- Agent pubkey after restart matches pre-restart pubkey.
- contactHandle in postIntent() matches pre-restart contactHandle.
- HD derivation path produces deterministic results from same seed.
- Existing counterparty connections remain valid.
Category: Unicity SDK Integration
Category 18: Semantic Search Verification
T18.1: Canonical description round-trips correctly
Test name: should produce and parse canonical description format without data loss
Preconditions:
- Intent params: direction='sell', base_asset='ALPHA', quote_asset='USDC', rate_min=450, rate_max=500, volume_min=100, volume_max=1000, escrow_address='any', deposit_timeout_sec=300.
Steps:
- Generate canonical description from intent params.
- Parse the generated description back into structured fields.
- Compare parsed fields against original params.
Expected outcome:
- Generated description:
"Selling 100-1000 ALPHA for USDC. Rate: 450-500 USDC per ALPHA. Escrow: any. Deposit timeout: 300s.". - Parsed fields match originals: direction='sell', volumes=[100,1000], assets=['ALPHA','USDC'], rates=[450,500], escrow='any', timeout=300.
- No data lost in round-trip.
Category: Semantic Search Verification
T18.2: Semantic search matches meaning not exact keywords
Test name: should match intents via semantic meaning rather than exact keyword matching
Preconditions:
- Posted intent description:
"Selling 100-1000 ALPHA for USDC. Rate: 450-500 USDC per ALPHA.". - Search query:
"buying ALPHA tokens with USDC stablecoin"(different wording, same semantic meaning).
Steps:
- MockMarketModule.search() called with semantic query derived from buy intent.
- Search engine matches by meaning (opposite direction, same asset pair).
Expected outcome:
- Search returns the posted sell intent as a match.
- Score >= min_search_score threshold.
- Client-side matching validates the result (opposite direction, overlapping rates).
- Semantic matching enables discovery even when exact description format differs.
Category: Semantic Search Verification
T18.3: search() results with own pubkey filtered out
Test name: should filter out search results where agentPublicKey matches own pubkey
Preconditions:
- Trader A has pubkey PK_A.
- MockMarketModule.search() returns 3 results: one from PK_A (own intent), one from PK_B, one from PK_C.
Steps:
- IntentEngine processes search results.
- Filters applied including self-match prevention.
Expected outcome:
- Result from PK_A filtered out (criterion 8: different agents).
- Only results from PK_B and PK_C considered for matching.
- No self-negotiation attempted.
Category: Semantic Search Verification
Note: Extends T2.7 (self-matching prevention). This test validates the filtering at the search result level specifically.
T18.4: subscribeFeed() reconnection after disconnect
Test name: should reconnect subscribeFeed() after WebSocket disconnection
Preconditions:
- subscribeFeed() initially connected and receiving feed events.
vi.useFakeTimers().
Steps:
- subscribeFeed() onError callback fires (WebSocket disconnection).
- Reconnection timer starts.
- Advance time by reconnection interval.
- subscribeFeed() re-invoked.
- New feed event arrives on reconnected subscription.
Expected outcome:
- Disconnection detected and logged as warning.
- Automatic reconnection attempted after backoff interval.
- New subscription established successfully.
- Feed events resume on reconnected subscription.
- Periodic search() continues as fallback during disconnection gap.
Category: Semantic Search Verification
Note: Extends T12.4 (which tests fallback to periodic search). This test validates the reconnection behavior.
T18.5: Stale search result gracefully rejected during negotiation
Test name: should gracefully handle stale search result where counterparty intent no longer exists
Preconditions:
- search() returned a result from Trader B 5 seconds ago.
- Trader B has since cancelled their intent.
- Trader A initiates negotiation based on stale result.
Steps:
- Trader A sends
np.propose_dealto Trader B. - Trader B receives proposal but intent is CANCELLED.
- Trader B sends
np.reject_dealwith reason_codeOTHERor does not respond (timeout).
Expected outcome:
- If Trader B responds with rejection: deal transitions to CANCELLED, volume released, intent returns to ACTIVE.
- If Trader B does not respond (offline/cancelled): 30s proposal timeout fires, deal cancelled, volume released.
- No crash or unhandled error from stale data.
- Intent returns to ACTIVE for re-matching with fresh search results.
Category: Semantic Search Verification
Test Count Summary
| Category | Count |
|---|---|
| 1. Intent Lifecycle (Happy Path) | 7 |
| 2. Intent Matching | 9 |
| 3. NP-0 Negotiation (Happy Path) | 3 |
| 4. NP-0 Negotiation (Unhappy Path) | 10 |
| 5. Swap Execution (Happy Path) | 6 |
| 6. Swap Execution (Unhappy Path) | 8 |
| 7. Partial Fill Scenarios | 4 |
| 8. Volume Reservation | 5 |
| 9. State Persistence | 5 |
| 10. ACP Command Validation | 18 |
| 11. Security Scenarios | 11 |
| 12. MarketModule Integration | 6 |
| 13. Multi-Agent Scenarios | 4 |
| Appendix: Edge Cases | 10 |
| 15. Adversarial Counterparty Attacks | 8 |
| 16. State Machine Violation Tests | 4 |
| 17. Unicity SDK Integration | 14 |
| 18. Semantic Search Verification | 5 |
| Total | 137 |