Skip to main content

crucible_test_context/
lib.rs

1use anchor_lang::prelude::sysvar::SysvarId;
2use litesvm::LiteSVM;
3use std::collections::{HashMap, HashSet};
4use std::rc::Rc;
5use std::sync::Arc;
6
7// Fast hashing for hot-path coverage collections (10-50x faster than SipHash for integers)
8pub use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
9
10/// Type alias for fast HashSet (uses FxHash)
11pub type FastHashSet<T> = FxHashSet<T>;
12/// Type alias for fast HashMap (uses FxHash)
13pub type FastHashMap<K, V> = FxHashMap<K, V>;
14use solana_account::Account;
15use solana_keypair::Keypair;
16use solana_message::inner_instruction::InnerInstructionsList;
17use solana_pubkey::Pubkey;
18use solana_signature::Signature;
19use solana_signer::Signer;
20use solana_transaction_context::TransactionReturnData;
21use solana_transaction_error::TransactionError;
22
23// Re-export types from anchor-lang for anchor program interactions
24pub use crate::account_builders::AccountBuilderBase;
25pub use crate::account_builders::GenericAccountBuilder;
26pub use crate::account_builders::MintAccountBuilder;
27pub use crate::account_builders::TokenAccountBuilder;
28pub use crate::instruction_builder::InstructionBuilder;
29pub use crate::mock_oracles::{
30    MockPythOracleBuilder, PriceFeedMessage, PriceUpdateV2, VerificationLevel,
31    DEFAULT_PYTH_RECEIVER_ID, PYTH_DISCRIMINATOR,
32};
33pub use crate::program_builder::ProgramBuilder;
34pub use crate::transaction_builder::TransactionBuilder;
35use anchor_lang::prelude::{Clock, Rent};
36use anchor_lang::solana_program::instruction::Instruction;
37use anchor_lang::solana_program::program_pack::Pack;
38use anchor_lang::solana_program::system_program;
39use anchor_lang::{AnchorDeserialize, AnchorSerialize, Discriminator};
40use anyhow::{Context, Result};
41use spl_token::solana_program::program_option::COption;
42
43mod account_builders;
44mod instruction_builder;
45mod program_builder;
46pub mod schema;
47pub mod snapshot;
48
49// Re-export schema types for generated code (register_schemas())
50pub use schema::{register_account_schemas, AccountSchema};
51mod transaction_builder;
52
53// Coverage analysis and visualization
54pub mod coverage;
55
56// Re-export coverage types for backward compatibility
57pub use coverage::{
58    build_cached_analysis, extract_functions, generate_bytecode_lcov, generate_coverage_html,
59    generate_coverage_html_cached, generate_source_lcov,
60};
61pub use coverage::{build_dwarf_source_map, DwarfSourceMap, SourceLocation};
62pub use coverage::{
63    CachedFunctionInfo, CachedProgramAnalysis, CoverageStats, CoverageWriteStats, FunctionInfo,
64    ReachableAnalysis,
65};
66
67pub use litesvm::InvocationInspectCallback;
68// Re-export litesvm for generated code (stateful mode creates placeholder SVMs)
69pub use litesvm;
70
71// Re-export serde_json for generated code
72pub use serde_json;
73
74// RPC account cloning (optional, requires `rpc-clone` feature)
75#[cfg(feature = "rpc-clone")]
76pub mod rpc_clone;
77#[cfg(feature = "rpc-clone")]
78pub use rpc_clone::AccountCloner;
79
80// ============================================================================
81// Global Action Counter (for monitor: actions/exec metric)
82// ============================================================================
83
84/// Global counter of total actions dispatched across all iterations.
85/// Used with TOTAL_EXECUTIONS to compute average actions per execution.
86pub static TOTAL_ACTIONS_DISPATCHED: std::sync::atomic::AtomicU64 =
87    std::sync::atomic::AtomicU64::new(0);
88
89/// Global counter of total actions that succeeded across all iterations.
90/// Used by monitor to display success rate (ok/total).
91pub static TOTAL_ACTIONS_SUCCEEDED: std::sync::atomic::AtomicU64 =
92    std::sync::atomic::AtomicU64::new(0);
93
94/// Total number of action variants available (set once at startup).
95/// Used by monitor to display "discovered: N/M actions".
96pub static TOTAL_ACTION_VARIANTS: std::sync::atomic::AtomicUsize =
97    std::sync::atomic::AtomicUsize::new(0);
98
99/// Increment the global action counter by one.
100/// Also increments the thread-local per-iteration counter for accurate
101/// multicore exec/sec reporting (global atomic has cross-thread noise).
102#[inline]
103pub fn increment_action_count() {
104    TOTAL_ACTIONS_DISPATCHED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
105    ITERATION_DISPATCH_COUNT.with(|c| c.set(c.get() + 1));
106}
107
108/// Increment the global succeeded action counter by one.
109#[inline]
110pub fn increment_action_success_count() {
111    TOTAL_ACTIONS_SUCCEEDED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
112}
113
114// ============================================================================
115// Thread-Local State
116// ============================================================================
117
118use serde::{Deserialize, Serialize};
119use std::cell::{Cell, RefCell};
120
121thread_local! {
122    // Invariant violation tracking for fuzz_assert! macros
123    static VIOLATION: RefCell<Option<String>> = RefCell::new(None);
124    // Per-instruction coverage tracking
125    static CURRENT_INSTRUCTION: RefCell<Option<String>> = RefCell::new(None);
126    // Crash metadata
127    static ACTION_HISTORY: RefCell<Vec<ActionRecord>> = RefCell::new(Vec::new());
128    static CURRENT_TEST_NAME: RefCell<Option<String>> = RefCell::new(None);
129    static CURRENT_ITERATION: RefCell<u64> = RefCell::new(0);
130    // Early exit tracking: total actions in sequence and which action triggered the violation
131    static TOTAL_ACTIONS_IN_SEQUENCE: RefCell<usize> = RefCell::new(0);
132    static VIOLATION_ACTION_INDEX: RefCell<Option<usize>> = RefCell::new(None);
133    // Error code passthrough from tx_result_to_outcome to push_action_record
134    static LAST_ERROR_CODE: Cell<Option<u32>> = const { Cell::new(None) };
135    // Per-iteration action dispatch count (thread-local, no cross-thread noise).
136    // Reset before each fn_name() call, read after to get accurate per-iteration count.
137    static ITERATION_DISPATCH_COUNT: Cell<u64> = const { Cell::new(0) };
138    // Success-seeking: tracks which action variant indices have ever succeeded
139    static SUCCEEDED_VARIANTS: RefCell<HashSet<usize>> = RefCell::new(HashSet::new());
140    // Cached env var: FUZZ_DEBUG (checked once per thread, avoids syscall on every send_batch)
141    static FUZZ_DEBUG: Cell<bool> = Cell::new(false);
142    // Stateful chain mode: when true, action loops break on first failure
143    static STATEFUL_CHAIN_MODE: Cell<bool> = const { Cell::new(false) };
144    // Corpus loading phase: suppress monitor output during loading
145    static CORPUS_LOADING: Cell<bool> = const { Cell::new(false) };
146}
147
148// Sentinel: has FUZZ_DEBUG TLS been initialized on this thread?
149thread_local! {
150    static FUZZ_DEBUG_INIT: Cell<bool> = const { Cell::new(false) };
151}
152
153/// Check FUZZ_DEBUG env var (cached per-thread after first call).
154#[inline]
155fn is_fuzz_debug() -> bool {
156    FUZZ_DEBUG_INIT.with(|init| {
157        if !init.get() {
158            let val = std::env::var("FUZZ_DEBUG").is_ok();
159            FUZZ_DEBUG.with(|c| c.set(val));
160            init.set(true);
161            val
162        } else {
163            FUZZ_DEBUG.with(|c| c.get())
164        }
165    })
166}
167
168/// Set stateful chain mode flag. When true, invariant test loops break on
169/// first action failure (failed actions produce dead-end states in stateful mode).
170#[inline]
171pub fn set_stateful_chain_mode(v: bool) {
172    STATEFUL_CHAIN_MODE.with(|f| f.set(v));
173}
174
175/// Check if stateful chain mode is active.
176#[inline]
177pub fn is_stateful_chain_mode() -> bool {
178    STATEFUL_CHAIN_MODE.with(|f| f.get())
179}
180
181/// Check FUZZ_DEBUG_REPLAY env var (cached per-thread).
182#[inline]
183pub fn is_debug_replay() -> bool {
184    // Simple check — not on hot path (only used in replay mode)
185    std::env::var("FUZZ_DEBUG_REPLAY").is_ok()
186}
187
188/// Hash SVM state for tracked accounts. Returns (hash, clock_slot).
189/// Accounts sorted by (lamports, data_len, data_hash) for cross-run stability.
190pub fn compute_svm_debug_hash(
191    svm: &litesvm::LiteSVM,
192    tracked_accounts: &[solana_pubkey::Pubkey],
193) -> (u64, u64) {
194    use anchor_lang::prelude::Clock;
195    use rustc_hash::FxHasher;
196    use std::hash::{Hash, Hasher};
197    let mut hasher = FxHasher::default();
198    let clock: Clock = svm.get_sysvar();
199    let slot = clock.slot;
200    clock.slot.hash(&mut hasher);
201    clock.epoch.hash(&mut hasher);
202    let mut entries: Vec<(u64, usize, u64)> = Vec::with_capacity(tracked_accounts.len());
203    for pubkey in tracked_accounts {
204        if let Some(acct) = svm.get_account(pubkey) {
205            let mut dh = FxHasher::default();
206            acct.data.hash(&mut dh);
207            entries.push((acct.lamports, acct.data.len(), dh.finish()));
208        }
209    }
210    entries.sort();
211    for (lamports, data_len, data_hash) in &entries {
212        lamports.hash(&mut hasher);
213        data_len.hash(&mut hasher);
214        data_hash.hash(&mut hasher);
215    }
216    (hasher.finish(), slot)
217}
218
219/// Set corpus loading flag (suppresses monitor output during loading).
220pub fn set_corpus_loading(v: bool) {
221    CORPUS_LOADING.with(|f| f.set(v));
222}
223
224/// Check if corpus loading is in progress.
225#[inline]
226pub fn is_corpus_loading() -> bool {
227    CORPUS_LOADING.with(|f| f.get())
228}
229
230#[doc(hidden)]
231/// Set the current Anchor instruction name (for coverage tracking)
232pub fn set_current_instruction(name: Option<String>) {
233    CURRENT_INSTRUCTION.with(|c| {
234        *c.borrow_mut() = name;
235    });
236}
237
238/// Get the current Anchor instruction name
239pub fn get_current_instruction() -> Option<String> {
240    CURRENT_INSTRUCTION.with(|c| c.borrow().clone())
241}
242
243/// Set the last error code from a transaction result (called by tx_result_to_outcome)
244pub fn set_last_error_code(code: Option<u32>) {
245    LAST_ERROR_CODE.with(|c| c.set(code));
246}
247
248/// Take the last error code, resetting it to None (called by push_action_record)
249fn take_last_error_code() -> Option<u32> {
250    LAST_ERROR_CODE.with(|c| c.replace(None))
251}
252
253// ============================================================================
254// Action History Tracking (for .meta.json crash metadata)
255// ============================================================================
256
257/// A single field-level semantic diff (used by schema registry for rich crash output).
258#[derive(Clone, Debug, Serialize, Deserialize)]
259pub struct FieldDelta {
260    pub field: String,
261    pub old_value: String,
262    pub new_value: String,
263}
264
265/// Record of a single action execution for crash metadata
266#[derive(Clone, Debug, Serialize, Deserialize)]
267pub struct ActionRecord {
268    /// Action name (e.g., "action_deposit")
269    pub name: String,
270    /// Action parameters as JSON object (with constrained/modulo'd values)
271    pub params: serde_json::Value,
272    /// Whether the action succeeded (from Result return value)
273    pub success: bool,
274    /// Error code from the last transaction (e.g., Custom(6051) → Some(6051))
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub error_code: Option<u32>,
277}
278
279/// Complete crash metadata for .meta.json files
280#[derive(Clone, Debug, Serialize, Deserialize)]
281pub struct CrashMetadata {
282    /// Name of the test that was running
283    pub test_name: String,
284    /// Timestamp when crash was detected (ISO 8601)
285    pub timestamp: String,
286    /// Fuzzer iteration number
287    pub iteration: u64,
288    /// Fuzzer seed (if available)
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub seed: Option<u64>,
291    /// Sequence of actions that led to the crash
292    pub actions: Vec<ActionRecord>,
293}
294
295/// Set the current test name (called at test start)
296pub fn set_current_test_name(name: &str) {
297    CURRENT_TEST_NAME.with(|t| {
298        *t.borrow_mut() = Some(name.to_string());
299    });
300}
301
302/// Get the current test name
303pub fn get_current_test_name() -> Option<String> {
304    CURRENT_TEST_NAME.with(|t| t.borrow().clone())
305}
306
307/// Set the current iteration number
308pub fn set_current_iteration(iteration: u64) {
309    CURRENT_ITERATION.with(|i| {
310        *i.borrow_mut() = iteration;
311    });
312}
313
314/// Get the current iteration number
315pub fn get_current_iteration() -> u64 {
316    CURRENT_ITERATION.with(|i| *i.borrow())
317}
318
319/// Push an action record to the history and update cumulative stats.
320pub fn push_action_record(name: &str, params: serde_json::Value, success: bool) {
321    let error_code = take_last_error_code();
322    ACTION_HISTORY.with(|h| {
323        h.borrow_mut().push(ActionRecord {
324            name: name.to_string(),
325            params,
326            success,
327            error_code,
328        });
329    });
330}
331
332/// Lite action record: only records name and success, defers JSON params.
333/// Avoids serde_json::Value allocation on every action of every iteration.
334/// The params field is set to `null` and should be backfilled via
335/// `backfill_action_params` only when needed (crash/violation).
336pub fn push_action_record_lite(name: &str, success: bool) {
337    let error_code = take_last_error_code();
338    ACTION_HISTORY.with(|h| {
339        h.borrow_mut().push(ActionRecord {
340            name: name.to_string(),
341            params: serde_json::Value::Null,
342            success,
343            error_code,
344        });
345    });
346}
347
348/// Backfill the params for a specific action record in the history.
349pub fn backfill_action_params(index: usize, params: serde_json::Value) {
350    ACTION_HISTORY.with(|h| {
351        let mut history = h.borrow_mut();
352        if let Some(record) = history.get_mut(index) {
353            record.params = params;
354        }
355    });
356}
357
358/// Get a copy of the action history
359pub fn get_action_history() -> Vec<ActionRecord> {
360    ACTION_HISTORY.with(|h| h.borrow().clone())
361}
362
363/// Check if the first action in history succeeded.
364/// Avoids cloning the entire history Vec — O(1) instead of O(n).
365#[inline]
366pub fn get_first_action_success() -> Option<bool> {
367    ACTION_HISTORY.with(|h| h.borrow().first().map(|r| r.success))
368}
369
370/// Clear the action history (called at start of each iteration)
371pub fn clear_action_history() {
372    ACTION_HISTORY.with(|h| h.borrow_mut().clear());
373}
374
375/// Reset the per-iteration dispatch counter (call before fn_name).
376#[inline]
377pub fn reset_iteration_dispatch_count() {
378    ITERATION_DISPATCH_COUNT.with(|c| c.set(0));
379}
380
381/// Get the per-iteration dispatch count (call after fn_name).
382/// Returns at least 1 to avoid zero-counting iterations.
383#[inline]
384pub fn get_iteration_dispatch_count() -> u64 {
385    ITERATION_DISPATCH_COUNT.with(|c| c.get().max(1))
386}
387
388// ============================================================================
389// send_batch sub-phase profiling (TLS accumulators)
390// ============================================================================
391
392thread_local! {
393    pub(crate) static SEND_BATCH_PRE_NS: Cell<u64> = const { Cell::new(0) };   // dirty_tracker
394    pub(crate) static SEND_BATCH_SVM_NS: Cell<u64> = const { Cell::new(0) };   // send_transaction (total)
395    pub(crate) static SEND_BATCH_POST_NS: Cell<u64> = const { Cell::new(0) };  // tx_result_to_outcome
396    // Sub-breakdown of send_transaction:
397    pub(crate) static SEND_TX_BLOCKHASH_NS: Cell<u64> = const { Cell::new(0) }; // expire_blockhash + latest_blockhash
398    pub(crate) static SEND_TX_SIGN_NS: Cell<u64> = const { Cell::new(0) };      // message + signing
399    pub(crate) static SEND_TX_EXEC_NS: Cell<u64> = const { Cell::new(0) };      // svm.send_transaction
400}
401
402/// Reset send_batch sub-phase accumulators (call before fn_name).
403#[inline]
404pub fn reset_send_batch_timers() {
405    SEND_BATCH_PRE_NS.with(|c| c.set(0));
406    SEND_BATCH_SVM_NS.with(|c| c.set(0));
407    SEND_BATCH_POST_NS.with(|c| c.set(0));
408    SEND_TX_BLOCKHASH_NS.with(|c| c.set(0));
409    SEND_TX_SIGN_NS.with(|c| c.set(0));
410    SEND_TX_EXEC_NS.with(|c| c.set(0));
411}
412
413/// Get send_batch sub-phase timers: (pre_ns, svm_ns, post_ns).
414#[inline]
415pub fn get_send_batch_timers() -> (u64, u64, u64) {
416    (
417        SEND_BATCH_PRE_NS.with(|c| c.get()),
418        SEND_BATCH_SVM_NS.with(|c| c.get()),
419        SEND_BATCH_POST_NS.with(|c| c.get()),
420    )
421}
422
423/// Get send_transaction sub-breakdown: (blockhash_ns, sign_ns, exec_ns).
424#[inline]
425pub fn get_send_tx_breakdown() -> (u64, u64, u64) {
426    (
427        SEND_TX_BLOCKHASH_NS.with(|c| c.get()),
428        SEND_TX_SIGN_NS.with(|c| c.get()),
429        SEND_TX_EXEC_NS.with(|c| c.get()),
430    )
431}
432
433/// Set the total number of actions in the current sequence (for early exit tracking)
434pub fn set_total_actions(count: usize) {
435    TOTAL_ACTIONS_IN_SEQUENCE.with(|t| *t.borrow_mut() = count);
436}
437
438/// Record which action index triggered a violation (only records first violation)
439pub fn set_violation_action_index(idx: usize) {
440    VIOLATION_ACTION_INDEX.with(|v| {
441        let mut guard = v.borrow_mut();
442        if guard.is_none() {
443            *guard = Some(idx);
444        }
445    });
446}
447
448/// Get the action index that triggered the violation (if any)
449pub fn get_violation_action_index() -> Option<usize> {
450    VIOLATION_ACTION_INDEX.with(|v| *v.borrow())
451}
452
453/// Clear all violation tracking state (called at start of each iteration)
454pub fn clear_violation_tracking() {
455    TOTAL_ACTIONS_IN_SEQUENCE.with(|t| *t.borrow_mut() = 0);
456    VIOLATION_ACTION_INDEX.with(|v| *v.borrow_mut() = None);
457}
458
459/// Clear all per-iteration state: action history + violation tracking.
460/// Convenience wrapper — call at the start of each fuzzing iteration.
461#[inline]
462pub fn clear_iteration_state() {
463    clear_action_history();
464    clear_violation_tracking();
465}
466
467// ============================================================================
468// Success-Seeking Mutation State
469// ============================================================================
470
471/// Check if a given action variant index has ever succeeded.
472pub fn has_variant_succeeded(variant_idx: usize) -> bool {
473    SUCCEEDED_VARIANTS.with(|s| s.borrow().contains(&variant_idx))
474}
475
476/// Mark an action variant index as having succeeded at least once.
477pub fn mark_variant_succeeded(variant_idx: usize) {
478    SUCCEEDED_VARIANTS.with(|s| {
479        s.borrow_mut().insert(variant_idx);
480    });
481}
482
483/// Get the number of action variants that have ever succeeded.
484pub fn succeeded_variant_count() -> usize {
485    SUCCEEDED_VARIANTS.with(|s| s.borrow().len())
486}
487
488/// Parse an action description string like "withdraw(authority=null, lamports=5) -> OK"
489/// back into an ActionRecord. Used by stateful mode to reconstruct full crash metadata
490/// from pool descriptions.
491pub fn parse_action_desc(desc: &str) -> ActionRecord {
492    let (body, success) = if let Some(body) = desc.strip_suffix(" -> OK") {
493        (body, true)
494    } else if let Some(body) = desc.strip_suffix(" -> FAIL") {
495        (body, false)
496    } else {
497        (desc, true)
498    };
499    let (name, params) = if let Some(paren) = body.find('(') {
500        let name = &body[..paren];
501        let params_str = body[paren + 1..].trim_end_matches(')');
502        let mut map = serde_json::Map::new();
503        for part in params_str.split(", ") {
504            if let Some(eq) = part.find('=') {
505                let key = &part[..eq];
506                let val_str = &part[eq + 1..];
507                let val = if val_str == "null" {
508                    serde_json::Value::Null
509                } else if val_str == "true" {
510                    serde_json::Value::Bool(true)
511                } else if val_str == "false" {
512                    serde_json::Value::Bool(false)
513                } else if let Ok(n) = val_str.parse::<u64>() {
514                    serde_json::Value::Number(n.into())
515                } else if let Ok(n) = val_str.parse::<i64>() {
516                    serde_json::Value::Number(n.into())
517                } else {
518                    serde_json::Value::String(val_str.to_string())
519                };
520                map.insert(key.to_string(), val);
521            }
522        }
523        (name.to_string(), serde_json::Value::Object(map))
524    } else {
525        (
526            body.to_string(),
527            serde_json::Value::Object(serde_json::Map::new()),
528        )
529    };
530    ActionRecord {
531        name,
532        params,
533        success,
534        error_code: None,
535    }
536}
537
538/// Build crash metadata from current state
539pub fn build_crash_metadata(seed: Option<u64>) -> CrashMetadata {
540    let timestamp = chrono_lite_timestamp();
541    CrashMetadata {
542        test_name: get_current_test_name().unwrap_or_else(|| "unknown".to_string()),
543        timestamp,
544        iteration: get_current_iteration(),
545        seed,
546        actions: get_action_history(),
547    }
548}
549
550/// Print the action sequence to stderr (for debugging crashes)
551/// Format the current action sequence from TLS history into a string.
552/// Must be called on the same thread that executed the actions (reads TLS).
553pub fn format_action_sequence() -> String {
554    use std::fmt::Write;
555
556    let history = get_action_history();
557    let total_actions = TOTAL_ACTIONS_IN_SEQUENCE.with(|t| *t.borrow());
558    let violation_idx = get_violation_action_index();
559
560    if history.is_empty() && total_actions == 0 {
561        return String::new();
562    }
563
564    let executed = history.len();
565    let skipped = total_actions.saturating_sub(executed);
566
567    let mut out = String::new();
568    let _ = writeln!(
569        out,
570        "\n=== FUZZ SEQUENCE ({} executed, {} skipped) ===",
571        executed, skipped
572    );
573
574    for (i, record) in history.iter().enumerate() {
575        let params_str = if let serde_json::Value::Object(map) = &record.params {
576            map.iter()
577                .map(|(k, v)| format!("{}={}", k, format_json_value(v)))
578                .collect::<Vec<_>>()
579                .join(", ")
580        } else {
581            String::new()
582        };
583
584        let status = if record.success { "OK" } else { "FAIL" };
585        let violation_marker = if violation_idx == Some(i) {
586            " [VIOLATION]"
587        } else {
588            ""
589        };
590
591        if params_str.is_empty() {
592            let _ = writeln!(
593                out,
594                "  {}. {} -> {}{}",
595                i + 1,
596                record.name,
597                status,
598                violation_marker
599            );
600        } else {
601            let _ = writeln!(
602                out,
603                "  {}. {}({}) -> {}{}",
604                i + 1,
605                record.name,
606                params_str,
607                status,
608                violation_marker
609            );
610        }
611    }
612
613    if skipped > 0 {
614        let _ = writeln!(
615            out,
616            "  ... {} action(s) not executed (stopped on violation)",
617            skipped
618        );
619    }
620
621    let _ = writeln!(out, "================================");
622    out
623}
624
625pub fn print_action_sequence() {
626    let s = format_action_sequence();
627    if !s.is_empty() {
628        eprint!("{}", s);
629    }
630}
631
632/// Format a JSON value for display (compact format)
633pub fn format_json_value(v: &serde_json::Value) -> String {
634    match v {
635        serde_json::Value::Null => "null".to_string(),
636        serde_json::Value::Bool(b) => b.to_string(),
637        serde_json::Value::Number(n) => n.to_string(),
638        serde_json::Value::String(s) => format!("\"{}\"", s),
639        serde_json::Value::Array(arr) => {
640            let items: Vec<String> = arr.iter().map(format_json_value).collect();
641            format!("[{}]", items.join(", "))
642        }
643        serde_json::Value::Object(obj) => {
644            let items: Vec<String> = obj
645                .iter()
646                .map(|(k, v)| format!("{}: {}", k, format_json_value(v)))
647                .collect();
648            format!("{{{}}}", items.join(", "))
649        }
650    }
651}
652
653/// Format the most recent action from TLS history as a one-line summary.
654/// e.g. "action_deposit(user=0, amount=500) -> OK"
655/// Returns empty string if no action in history.
656pub fn format_last_action_oneline() -> String {
657    let history = get_action_history();
658    match history.last() {
659        Some(record) => {
660            let params_str = if let serde_json::Value::Object(map) = &record.params {
661                map.iter()
662                    .map(|(k, v)| format!("{}={}", k, format_json_value(v)))
663                    .collect::<Vec<_>>()
664                    .join(", ")
665            } else {
666                String::new()
667            };
668            let status = if record.success { "OK" } else { "FAIL" };
669            if params_str.is_empty() {
670                format!("{} -> {}", record.name, status)
671            } else {
672                format!("{}({}) -> {}", record.name, params_str, status)
673            }
674        }
675        None => String::new(),
676    }
677}
678
679/// Format ALL actions in the current history as one-line descriptions, newline-separated.
680/// Used by stateful mode to store the full chain description in a single `action_desc` field.
681pub fn format_all_actions_oneline() -> String {
682    let history = get_action_history();
683    let mut lines = Vec::with_capacity(history.len());
684    for record in &history {
685        let params_str = if let serde_json::Value::Object(map) = &record.params {
686            map.iter()
687                .map(|(k, v)| format!("{}={}", k, format_json_value(v)))
688                .collect::<Vec<_>>()
689                .join(", ")
690        } else {
691            String::new()
692        };
693        let status = if record.success { "OK" } else { "FAIL" };
694        if params_str.is_empty() {
695            lines.push(format!("{} -> {}", record.name, status));
696        } else {
697            lines.push(format!("{}({}) -> {}", record.name, params_str, status));
698        }
699    }
700    lines.join("\n")
701}
702
703/// Write crash metadata to a .meta.json file and save input bytes for replay
704pub fn write_crash_metadata(
705    crash_dir: &str,
706    input_hash: u64,
707    seed: Option<u64>,
708    input_bytes: &[u8],
709) {
710    write_crash_metadata_with_actions(crash_dir, input_hash, seed, input_bytes, None)
711}
712
713/// Write crash metadata with an explicit full action chain (for stateful mode).
714/// If `full_actions` is Some, it overrides the TLS action history.
715pub fn write_crash_metadata_with_actions(
716    crash_dir: &str,
717    input_hash: u64,
718    seed: Option<u64>,
719    input_bytes: &[u8],
720    full_actions: Option<Vec<ActionRecord>>,
721) {
722    let crash_id = format!("crash_{:016x}", input_hash);
723    let mut metadata = build_crash_metadata(seed);
724    if let Some(actions) = full_actions {
725        metadata.actions = actions;
726    }
727    let meta_dir = std::env::var("FUZZ_META_DIR").unwrap_or_else(|_| crash_dir.to_string());
728    let meta_filename = format!("{}/{}.meta.json", meta_dir, crash_id);
729    let input_filename = format!("{}/{}", crash_dir, crash_id);
730
731    // Save the input bytes for replay
732    if let Err(e) = std::fs::write(&input_filename, input_bytes) {
733        eprintln!(
734            "[META] Failed to write crash input {}: {}",
735            input_filename, e
736        );
737    }
738
739    // Save the metadata
740    match serde_json::to_string_pretty(&metadata) {
741        Ok(json) => {
742            if let Err(e) = std::fs::write(&meta_filename, json) {
743                eprintln!("[META] Failed to write {}: {}", meta_filename, e);
744            }
745        }
746        Err(e) => {
747            eprintln!("[META] Failed to serialize metadata: {}", e);
748        }
749    }
750}
751
752/// Write crash metadata for a known crash ID (used by tmin to update metadata in place)
753pub fn write_crash_metadata_for_id(crash_dir: &str, crash_id: &str, seed: Option<u64>) {
754    let metadata = build_crash_metadata(seed);
755    let meta_filename = format!("{}/{}.meta.json", crash_dir, crash_id);
756
757    match serde_json::to_string_pretty(&metadata) {
758        Ok(json) => {
759            if let Err(e) = std::fs::write(&meta_filename, json) {
760                eprintln!("[META] Failed to write {}: {}", meta_filename, e);
761            }
762        }
763        Err(e) => {
764            eprintln!("[META] Failed to serialize metadata: {}", e);
765        }
766    }
767}
768
769// ============================================================================
770// Action Result Trait (for Feature 2: actions return Result)
771// ============================================================================
772
773/// Trait to normalize action return values to success/failure.
774/// Allows actions to return either `()` (always success) or `Result<(), E>` (success/failure).
775pub trait IntoActionSuccess {
776    fn into_success(self) -> bool;
777}
778
779impl IntoActionSuccess for () {
780    fn into_success(self) -> bool {
781        true
782    }
783}
784
785impl<T, E> IntoActionSuccess for Result<T, E> {
786    fn into_success(self) -> bool {
787        self.is_ok()
788    }
789}
790
791impl IntoActionSuccess for bool {
792    fn into_success(self) -> bool {
793        self
794    }
795}
796
797/// Simple timestamp function (avoids chrono dependency)
798fn chrono_lite_timestamp() -> String {
799    use std::time::{SystemTime, UNIX_EPOCH};
800    let now = SystemTime::now()
801        .duration_since(UNIX_EPOCH)
802        .unwrap_or_default();
803    let secs = now.as_secs();
804    // Basic ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
805    // This is a simplified calculation - not accounting for leap years perfectly
806    let days_since_epoch = secs / 86400;
807    let time_of_day = secs % 86400;
808    let hours = time_of_day / 3600;
809    let minutes = (time_of_day % 3600) / 60;
810    let seconds = time_of_day % 60;
811
812    // Approximate year/month/day calculation
813    let mut year = 1970;
814    let mut remaining_days = days_since_epoch as i64;
815    loop {
816        let days_in_year = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
817            366
818        } else {
819            365
820        };
821        if remaining_days < days_in_year {
822            break;
823        }
824        remaining_days -= days_in_year;
825        year += 1;
826    }
827
828    let days_in_months = if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) {
829        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
830    } else {
831        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
832    };
833
834    let mut month = 1;
835    for days_in_month in days_in_months {
836        if remaining_days < days_in_month as i64 {
837            break;
838        }
839        remaining_days -= days_in_month as i64;
840        month += 1;
841    }
842    let day = remaining_days + 1;
843
844    format!(
845        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
846        year, month, day, hours, minutes, seconds
847    )
848}
849
850// ============================================================================
851// Discriminator-based instruction detection
852// ============================================================================
853
854use std::sync::OnceLock;
855
856/// Global map from discriminator bytes to instruction name.
857/// Supports both 8-byte (Anchor/borsh) and 4-byte (native/bincode) discriminators.
858/// Populated once at harness startup via `register_instruction_discriminators()`.
859/// Uses OnceLock for lock-free reads after initialization (single-threaded).
860static DISCRIMINATOR_MAP: OnceLock<(usize, HashMap<Vec<u8>, String>)> = OnceLock::new();
861
862/// Register instruction discriminators for per-instruction coverage tracking.
863/// Call this once at harness initialization with discriminators from the program's IDL.
864/// Supports variable-length discriminators (4 bytes for bincode, 8 bytes for Anchor).
865/// Subsequent calls are ignored (OnceLock can only be set once).
866///
867/// Example:
868/// ```ignore
869/// register_instruction_discriminators(&[
870///     ("deposit", vec![171, 94, 235, 200, 28, 230, 215, 98]),
871///     ("borrow", vec![4, 126, 116, 45, 173, 75, 231, 84]),
872/// ]);
873/// ```
874pub fn register_instruction_discriminators(discriminators: &[(&str, Vec<u8>)]) {
875    // Determine discriminator length from first entry (all must be same length)
876    let disc_len = discriminators.first().map(|(_, d)| d.len()).unwrap_or(8);
877    let map: HashMap<Vec<u8>, String> = discriminators
878        .iter()
879        .map(|(name, disc)| (disc.clone(), name.to_string()))
880        .collect();
881    let _ = DISCRIMINATOR_MAP.set((disc_len, map));
882}
883
884/// Look up instruction name from discriminator bytes at the start of instruction data.
885/// Automatically uses the correct discriminator length (4 or 8 bytes).
886/// Returns None if discriminator is not registered or if the data is too short.
887/// Lock-free after initialization.
888pub fn lookup_instruction_by_discriminator(instruction_data: &[u8]) -> Option<String> {
889    let (disc_len, map) = DISCRIMINATOR_MAP.get()?;
890    if instruction_data.len() < *disc_len {
891        return None;
892    }
893    let disc = instruction_data[..*disc_len].to_vec();
894    map.get(&disc).cloned()
895}
896
897/// Get all registered discriminators (for debugging)
898pub fn get_registered_discriminators() -> Vec<(String, Vec<u8>)> {
899    DISCRIMINATOR_MAP
900        .get()
901        .map(|(_, map)| map.iter().map(|(k, v)| (v.clone(), k.clone())).collect())
902        .unwrap_or_default()
903}
904
905// ============================================================================
906
907/// Record an invariant violation (used by fuzz_assert! macros)
908pub fn record_violation(msg: String) {
909    VIOLATION.with(|v| {
910        // Use a single mutable borrow to avoid any RefCell borrow conflicts
911        let mut guard = v.borrow_mut();
912        if guard.is_none() {
913            *guard = Some(msg);
914        }
915    });
916}
917
918/// Take the current violation (clearing it). Returns Some if violated.
919pub fn take_violation() -> Option<String> {
920    VIOLATION.with(|v| v.borrow_mut().take())
921}
922
923/// Check if a violation has been recorded (without consuming it)
924pub fn has_violation() -> bool {
925    VIOLATION.with(|v| v.borrow().is_some())
926}
927
928/// Assert a condition is true
929#[macro_export]
930macro_rules! fuzz_assert {
931    ($cond:expr $(,)?) => {
932        if !($cond) {
933            $crate::record_violation(format!(
934                "Assertion failed: {} at {}:{}",
935                stringify!($cond), file!(), line!()
936            ));
937        }
938    };
939    ($cond:expr, $($arg:tt)+) => {
940        if !($cond) {
941            $crate::record_violation(format!($($arg)+));
942        }
943    };
944}
945
946/// Assert two values are equal
947#[macro_export]
948macro_rules! fuzz_assert_eq {
949    ($left:expr, $right:expr $(,)?) => {
950        if $left != $right {
951            $crate::record_violation(format!(
952                "Assertion failed: {} == {} ({:?} != {:?}) at {}:{}",
953                stringify!($left), stringify!($right), $left, $right, file!(), line!()
954            ));
955        }
956    };
957    ($left:expr, $right:expr, $($arg:tt)+) => {
958        if $left != $right {
959            $crate::record_violation(format!($($arg)+));
960        }
961    };
962}
963
964/// Assert two values are not equal
965#[macro_export]
966macro_rules! fuzz_assert_ne {
967    ($left:expr, $right:expr $(,)?) => {
968        if $left == $right {
969            $crate::record_violation(format!(
970                "Assertion failed: {} != {} ({:?} == {:?}) at {}:{}",
971                stringify!($left), stringify!($right), $left, $right, file!(), line!()
972            ));
973        }
974    };
975    ($left:expr, $right:expr, $($arg:tt)+) => {
976        if $left == $right {
977            $crate::record_violation(format!($($arg)+));
978        }
979    };
980}
981
982/// Assert a < b
983#[macro_export]
984macro_rules! fuzz_assert_lt {
985    ($left:expr, $right:expr $(,)?) => {
986        if !($left < $right) {
987            $crate::record_violation(format!(
988                "Assertion failed: {} < {} ({:?} >= {:?}) at {}:{}",
989                stringify!($left), stringify!($right), $left, $right, file!(), line!()
990            ));
991        }
992    };
993    ($left:expr, $right:expr, $($arg:tt)+) => {
994        if !($left < $right) {
995            $crate::record_violation(format!($($arg)+));
996        }
997    };
998}
999
1000/// Assert a <= b
1001#[macro_export]
1002macro_rules! fuzz_assert_le {
1003    ($left:expr, $right:expr $(,)?) => {
1004        if !($left <= $right) {
1005            $crate::record_violation(format!(
1006                "Assertion failed: {} <= {} ({:?} > {:?}) at {}:{}",
1007                stringify!($left), stringify!($right), $left, $right, file!(), line!()
1008            ));
1009        }
1010    };
1011    ($left:expr, $right:expr, $($arg:tt)+) => {
1012        if !($left <= $right) {
1013            $crate::record_violation(format!($($arg)+));
1014        }
1015    };
1016}
1017
1018/// Assert a > b
1019#[macro_export]
1020macro_rules! fuzz_assert_gt {
1021    ($left:expr, $right:expr $(,)?) => {
1022        if !($left > $right) {
1023            $crate::record_violation(format!(
1024                "Assertion failed: {} > {} ({:?} <= {:?}) at {}:{}",
1025                stringify!($left), stringify!($right), $left, $right, file!(), line!()
1026            ));
1027        }
1028    };
1029    ($left:expr, $right:expr, $($arg:tt)+) => {
1030        if !($left > $right) {
1031            $crate::record_violation(format!($($arg)+));
1032        }
1033    };
1034}
1035
1036/// Assert a >= b
1037#[macro_export]
1038macro_rules! fuzz_assert_ge {
1039    ($left:expr, $right:expr $(,)?) => {
1040        if !($left >= $right) {
1041            $crate::record_violation(format!(
1042                "Assertion failed: {} >= {} ({:?} < {:?}) at {}:{}",
1043                stringify!($left), stringify!($right), $left, $right, file!(), line!()
1044            ));
1045        }
1046    };
1047    ($left:expr, $right:expr, $($arg:tt)+) => {
1048        if !($left >= $right) {
1049            $crate::record_violation(format!($($arg)+));
1050        }
1051    };
1052}
1053
1054/// Assert two values are approximately equal within a delta (absolute difference)
1055#[macro_export]
1056macro_rules! fuzz_assert_approx_eq {
1057    ($left:expr, $right:expr, $delta:expr $(,)?) => {{
1058        let diff = if $left > $right { $left - $right } else { $right - $left };
1059        if diff > $delta {
1060            $crate::record_violation(format!(
1061                "Assertion failed: |{} - {}| <= {} (|{:?} - {:?}| = {:?} > {:?}) at {}:{}",
1062                stringify!($left), stringify!($right), stringify!($delta),
1063                $left, $right, diff, $delta, file!(), line!()
1064            ));
1065        }
1066    }};
1067    ($left:expr, $right:expr, $delta:expr, $($arg:tt)+) => {{
1068        let diff = if $left > $right { $left - $right } else { $right - $left };
1069        if diff > $delta {
1070            $crate::record_violation(format!($($arg)+));
1071        }
1072    }};
1073}
1074
1075// Mock oracles for testing
1076mod mock_oracles;
1077
1078/// Parsed transaction outcome from litesvm execution
1079#[derive(Debug, Clone)]
1080pub enum TxOutcome {
1081    /// Transaction executed successfully
1082    Success {
1083        compute_units: u64,
1084        logs: Vec<String>,
1085        signature: Signature,
1086        inner_instructions: InnerInstructionsList,
1087        return_data: TransactionReturnData,
1088        fee: u64,
1089    },
1090    /// Transaction failed with program error
1091    ProgramError {
1092        /// Raw error from SVM
1093        error: TransactionError,
1094        /// Parsed error code (e.g., 6051 from Custom(6051))
1095        error_code: Option<u32>,
1096        /// Instruction index that failed
1097        instruction_index: Option<u8>,
1098        logs: Vec<String>,
1099        signature: Signature,
1100        inner_instructions: InnerInstructionsList,
1101        return_data: TransactionReturnData,
1102        fee: u64,
1103    },
1104}
1105
1106/// Error type for TxOutcome::into_result()
1107#[derive(Debug, Clone)]
1108pub struct TxError {
1109    pub error: TransactionError,
1110    pub error_code: Option<u32>,
1111    pub instruction_index: Option<u8>,
1112    pub logs: Vec<String>,
1113    pub signature: Signature,
1114    pub inner_instructions: InnerInstructionsList,
1115    pub return_data: TransactionReturnData,
1116    pub fee: u64,
1117}
1118
1119impl std::error::Error for TxError {}
1120
1121impl std::fmt::Display for TxError {
1122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1123        write!(f, "Transaction failed")?;
1124        if let Some(code) = self.error_code {
1125            write!(f, " (error code: {})", code)?;
1126        }
1127        if let Some(idx) = self.instruction_index {
1128            write!(f, " at instruction {}", idx)?;
1129        }
1130        Ok(())
1131    }
1132}
1133
1134impl TxOutcome {
1135    pub fn is_success(&self) -> bool {
1136        matches!(self, TxOutcome::Success { .. })
1137    }
1138
1139    pub fn is_error(&self) -> bool {
1140        matches!(self, TxOutcome::ProgramError { .. })
1141    }
1142
1143    pub fn error_code(&self) -> Option<u32> {
1144        match self {
1145            TxOutcome::ProgramError { error_code, .. } => *error_code,
1146            _ => None,
1147        }
1148    }
1149
1150    pub fn logs(&self) -> &[String] {
1151        match self {
1152            TxOutcome::Success { logs, .. } => logs,
1153            TxOutcome::ProgramError { logs, .. } => logs,
1154        }
1155    }
1156
1157    pub fn compute_units(&self) -> Option<u64> {
1158        match self {
1159            TxOutcome::Success { compute_units, .. } => Some(*compute_units),
1160            _ => None,
1161        }
1162    }
1163
1164    pub fn signature(&self) -> &Signature {
1165        match self {
1166            TxOutcome::Success { signature, .. } => signature,
1167            TxOutcome::ProgramError { signature, .. } => signature,
1168        }
1169    }
1170
1171    pub fn fee(&self) -> u64 {
1172        match self {
1173            TxOutcome::Success { fee, .. } => *fee,
1174            TxOutcome::ProgramError { fee, .. } => *fee,
1175        }
1176    }
1177
1178    pub fn inner_instructions(&self) -> &InnerInstructionsList {
1179        match self {
1180            TxOutcome::Success {
1181                inner_instructions, ..
1182            } => inner_instructions,
1183            TxOutcome::ProgramError {
1184                inner_instructions, ..
1185            } => inner_instructions,
1186        }
1187    }
1188
1189    pub fn return_data(&self) -> &TransactionReturnData {
1190        match self {
1191            TxOutcome::Success { return_data, .. } => return_data,
1192            TxOutcome::ProgramError { return_data, .. } => return_data,
1193        }
1194    }
1195
1196    /// Unwrap success or panic with detailed error message including logs
1197    pub fn unwrap(self) {
1198        match self {
1199            TxOutcome::Success { .. } => {}
1200            TxOutcome::ProgramError {
1201                error,
1202                error_code,
1203                logs,
1204                ..
1205            } => {
1206                let mut msg = format!("Transaction failed: {:?}", error);
1207                if let Some(code) = error_code {
1208                    msg.push_str(&format!(" (code: {})", code));
1209                }
1210                msg.push_str("\nLogs:\n");
1211                for log in &logs {
1212                    msg.push_str(&format!("  {}\n", log));
1213                }
1214                panic!("{}", msg);
1215            }
1216        }
1217    }
1218
1219    /// Expect success or panic with custom message
1220    pub fn expect(self, msg: &str) {
1221        match self {
1222            TxOutcome::Success { .. } => {}
1223            TxOutcome::ProgramError { logs, .. } => {
1224                let mut full_msg = format!("{}\nLogs:\n", msg);
1225                for log in &logs {
1226                    full_msg.push_str(&format!("  {}\n", log));
1227                }
1228                panic!("{}", full_msg);
1229            }
1230        }
1231    }
1232
1233    /// Convert to Result for ? operator compatibility
1234    pub fn into_result(self) -> std::result::Result<(), TxError> {
1235        match self {
1236            TxOutcome::Success { .. } => Ok(()),
1237            TxOutcome::ProgramError {
1238                error,
1239                error_code,
1240                instruction_index,
1241                logs,
1242                signature,
1243                inner_instructions,
1244                return_data,
1245                fee,
1246            } => Err(TxError {
1247                error,
1248                error_code,
1249                instruction_index,
1250                logs,
1251                signature,
1252                inner_instructions,
1253                return_data,
1254                fee,
1255            }),
1256        }
1257    }
1258}
1259
1260/// Parse litesvm TransactionError to extract error code.
1261/// Extracts Custom(N) error codes from InstructionError variants.
1262pub fn parse_error_code(err: &TransactionError) -> Option<u32> {
1263    use solana_instruction::error::InstructionError;
1264    match err {
1265        TransactionError::InstructionError(_, InstructionError::Custom(code)) => Some(*code),
1266        _ => None,
1267    }
1268}
1269
1270/// Parse litesvm TransactionError to extract instruction index.
1271pub fn parse_instruction_index(err: &TransactionError) -> Option<u8> {
1272    match err {
1273        TransactionError::InstructionError(idx, _) => Some(*idx),
1274        _ => None,
1275    }
1276}
1277
1278/// Convert litesvm transaction result to TxOutcome
1279pub fn tx_result_to_outcome(result: litesvm::types::TransactionResult) -> TxOutcome {
1280    let outcome = match result {
1281        Ok(meta) => TxOutcome::Success {
1282            compute_units: meta.compute_units_consumed,
1283            logs: meta.logs,
1284            signature: meta.signature,
1285            inner_instructions: meta.inner_instructions,
1286            return_data: meta.return_data,
1287            fee: meta.fee,
1288        },
1289        Err(failed) => {
1290            let error_code = parse_error_code(&failed.err);
1291            let instruction_index = parse_instruction_index(&failed.err);
1292            TxOutcome::ProgramError {
1293                error: failed.err,
1294                error_code,
1295                instruction_index,
1296                logs: failed.meta.logs,
1297                signature: failed.meta.signature,
1298                inner_instructions: failed.meta.inner_instructions,
1299                return_data: failed.meta.return_data,
1300                fee: failed.meta.fee,
1301            }
1302        }
1303    };
1304    // Store error code in TLS for push_action_record to pick up
1305    set_last_error_code(outcome.error_code());
1306    outcome
1307}
1308
1309// Re-export types needed by generated code
1310pub mod fuzz_types {
1311    pub use solana_program_runtime::invoke_context::{Executable, InvokeContext};
1312    pub use solana_pubkey::Pubkey;
1313    pub use solana_sbpf::ebpf;
1314    pub use solana_sbpf::static_analysis::Analysis;
1315    pub use solana_transaction::sanitized::SanitizedTransaction;
1316    pub use solana_transaction_context::{IndexOfAccount, InstructionContext};
1317}
1318
1319/// Stores program data for reloading into debuggable SVMs
1320#[derive(Clone)]
1321pub struct ProgramData {
1322    pub program_id: Pubkey,
1323    pub data: Vec<u8>,
1324}
1325
1326pub struct TestContext {
1327    pub svm: LiteSVM,
1328    pub pending_instructions: Vec<Instruction>,
1329    pending_signers: Vec<Keypair>,
1330    /// Programs loaded into this context (for reloading into debuggable SVMs).
1331    /// Arc-wrapped so cloning doesn't deep-copy program binaries (~1-2MB each).
1332    programs: std::sync::Arc<Vec<ProgramData>>,
1333    /// Account pubkeys that have been set (for copying to debuggable SVMs)
1334    tracked_accounts: Arc<HashSet<Pubkey>>,
1335    /// Total CFG edges and instructions per program (for coverage percentage calculation).
1336    /// Arc-wrapped: set once during setup, shared cheaply across multicore fixture clones.
1337    /// Value is (total_edges, total_instructions).
1338    program_coverage_totals: Arc<HashMap<Pubkey, (usize, usize)>>,
1339    /// Snapshot of initial state for fast restore (always-on in fuzz mode)
1340    pub snapshot: Option<snapshot::SvmSnapshot>,
1341    /// Tracks dirty accounts across all transactions in an iteration
1342    pub dirty_tracker: snapshot::DirtyTracker,
1343    /// Whether signature verification is enabled on the SVM.
1344    /// When false (default for fuzzing), transactions use dummy signatures
1345    /// to skip ed25519 computation (~77µs per tx).
1346    pub sigverify: bool,
1347}
1348
1349impl Clone for TestContext {
1350    fn clone(&self) -> Self {
1351        Self {
1352            svm: self.svm.clone(),
1353            pending_instructions: self.pending_instructions.clone(),
1354            pending_signers: self
1355                .pending_signers
1356                .iter()
1357                .map(|k| k.insecure_clone())
1358                .collect(),
1359            programs: self.programs.clone(),
1360            tracked_accounts: self.tracked_accounts.clone(),
1361            program_coverage_totals: self.program_coverage_totals.clone(),
1362            // Snapshot is not cloned — only the template fixture owns it
1363            snapshot: None,
1364            // Fresh tracker for each clone
1365            dirty_tracker: self.dirty_tracker.clone(),
1366            sigverify: self.sigverify,
1367        }
1368    }
1369}
1370
1371/// Empty callback that does nothing - used during setup to avoid DefaultRegisterTracingCallback
1372/// trying to find .so files on disk for built-in programs.
1373pub struct EmptyInvocationCallback;
1374
1375impl InvocationInspectCallback for EmptyInvocationCallback {
1376    fn before_invocation(
1377        &self,
1378        _tx: &solana_transaction::sanitized::SanitizedTransaction,
1379        _program_indices: &[solana_transaction_context::IndexOfAccount],
1380        _invoke_context: &solana_program_runtime::invoke_context::InvokeContext,
1381    ) {
1382    }
1383
1384    fn after_invocation(
1385        &self,
1386        _invoke_context: &solana_program_runtime::invoke_context::InvokeContext,
1387        _register_tracing_enabled: bool,
1388    ) {
1389    }
1390}
1391
1392impl TestContext {
1393    pub fn new() -> Self {
1394        // When CRUCIBLE_FUZZ_DEBUGGABLE is set (by the fuzz macro), create a debuggable SVM
1395        // so programs are loaded with register tracing support baked in.
1396        // Use EmptyInvocationCallback to suppress "Error collecting register tracing" messages
1397        // from DefaultRegisterTracingCallback trying to find .so files for built-in programs.
1398        let svm = if std::env::var("CRUCIBLE_FUZZ_DEBUGGABLE").is_ok() {
1399            let mut svm = LiteSVM::new_debuggable(true)
1400                .with_transaction_history(0)
1401                .with_sigverify(false)
1402                .with_blockhash_check(false);
1403            svm.set_invocation_inspect_callback(EmptyInvocationCallback);
1404            svm
1405        } else {
1406            LiteSVM::new()
1407                .with_transaction_history(0)
1408                .with_sigverify(false)
1409                .with_blockhash_check(false)
1410        };
1411
1412        Self {
1413            svm,
1414            pending_instructions: Vec::new(),
1415            pending_signers: Vec::new(),
1416            programs: std::sync::Arc::new(Vec::new()),
1417            tracked_accounts: Arc::new(HashSet::new()),
1418            program_coverage_totals: Arc::new(HashMap::new()),
1419            snapshot: None,
1420            dirty_tracker: snapshot::DirtyTracker::new(),
1421            sigverify: false,
1422        }
1423    }
1424
1425    pub fn with_invocation_callback<C: InvocationInspectCallback + 'static>(callback: C) -> Self {
1426        let mut svm = LiteSVM::new_debuggable(true)
1427            .with_transaction_history(0)
1428            .with_sigverify(false)
1429            .with_blockhash_check(false);
1430        svm.set_invocation_inspect_callback(callback);
1431        Self {
1432            svm,
1433            pending_instructions: Vec::new(),
1434            pending_signers: Vec::new(),
1435            programs: std::sync::Arc::new(Vec::new()),
1436            tracked_accounts: Arc::new(HashSet::new()),
1437            program_coverage_totals: Arc::new(HashMap::new()),
1438            snapshot: None,
1439            dirty_tracker: snapshot::DirtyTracker::new(),
1440            sigverify: false,
1441        }
1442    }
1443
1444    pub fn with_compute_budget(mut self, compute_unit_limit: u64) -> Self {
1445        use solana_compute_budget::compute_budget::ComputeBudget;
1446        let mut budget = ComputeBudget::new_with_defaults(true, true);
1447        budget.compute_unit_limit = compute_unit_limit;
1448        self.svm = self.svm.with_compute_budget(budget);
1449        self
1450    }
1451
1452    /// Analyze a program binary and return (total_edges, total_instructions).
1453    /// Only counts reachable code from the program entrypoint via BFS.
1454    /// Only counts edges from conditional jump instructions (matching runtime tracking).
1455    /// Used for coverage percentage calculation.
1456    pub fn analyze_program_coverage(program_data: &[u8]) -> Option<(usize, usize)> {
1457        use solana_sbpf::ebpf;
1458        use solana_sbpf::elf::Executable;
1459        use solana_sbpf::program::BuiltinProgram;
1460        use solana_sbpf::static_analysis::Analysis;
1461        use solana_sbpf::vm::ContextObject;
1462
1463        struct DummyContext;
1464        impl ContextObject for DummyContext {
1465            fn consume(&mut self, _amount: u64) {}
1466            fn get_remaining(&self) -> u64 {
1467                0
1468            }
1469        }
1470
1471        let loader = Arc::new(BuiltinProgram::<DummyContext>::new_mock());
1472        let executable = Executable::from_elf(program_data, loader).ok()?;
1473        let analysis = Analysis::from_executable(&executable).ok()?;
1474
1475        // BFS from entrypoint + all registered function entries to find reachable CFG nodes.
1476        // Function entries are seeded because CALL targets are encoded in instruction
1477        // immediates, not in cfg_node.destinations.
1478        let mut visited = HashSet::new();
1479        let mut queue = std::collections::VecDeque::new();
1480
1481        // Seed with program entrypoint
1482        if analysis.cfg_nodes.contains_key(&analysis.entrypoint) {
1483            visited.insert(analysis.entrypoint);
1484            queue.push_back(analysis.entrypoint);
1485        }
1486
1487        // Seed all function entry points (reachable via CALL instructions).
1488        // analysis.functions maps PC → (key, name); cfg_nodes are keyed by node index.
1489        for (&pc, _) in &analysis.functions {
1490            if analysis.cfg_nodes.contains_key(&pc) {
1491                if visited.insert(pc) {
1492                    queue.push_back(pc);
1493                }
1494            }
1495        }
1496
1497        while let Some(node_id) = queue.pop_front() {
1498            if let Some(cfg_node) = analysis.cfg_nodes.get(&node_id) {
1499                for &dest in &cfg_node.destinations {
1500                    if visited.insert(dest) {
1501                        queue.push_back(dest);
1502                    }
1503                }
1504            }
1505        }
1506
1507        // Count edges and instructions only for reachable nodes
1508        let mut total_conditional: usize = 0;
1509        let mut total_instructions: usize = 0;
1510        for (&node_id, cfg_node) in &analysis.cfg_nodes {
1511            if !visited.contains(&node_id) {
1512                continue;
1513            }
1514
1515            total_instructions += cfg_node.instructions.end - cfg_node.instructions.start;
1516
1517            if cfg_node.instructions.is_empty() {
1518                continue;
1519            }
1520
1521            let last_insn = &analysis.instructions[cfg_node.instructions.end - 1];
1522            let is_jmp = last_insn.opc & 7 == ebpf::BPF_JMP;
1523            if is_jmp {
1524                let opc = last_insn.opc;
1525                let is_conditional = opc != 0x05 && opc != 0x85 && opc != 0x8d && opc != 0x95;
1526                if is_conditional {
1527                    total_conditional += cfg_node.destinations.len();
1528                }
1529            }
1530        }
1531
1532        Some((total_conditional, total_instructions))
1533    }
1534
1535    pub fn add_program(&mut self, program_id: &Pubkey, program_path: &str) -> Result<()> {
1536        let actual_path = if let Ok(override_path) = std::env::var("FUZZ_PROGRAM_SO") {
1537            eprintln!(
1538                "[COVERAGE] Program binary override: {} -> {}",
1539                program_path, override_path
1540            );
1541            override_path
1542        } else {
1543            program_path.to_string()
1544        };
1545        let program_data = std::fs::read(&actual_path)?;
1546        self.add_program_from_bytes(program_id, &program_data)
1547    }
1548
1549    /// Load a program from raw ELF bytes into the SVM.
1550    ///
1551    /// Same as `add_program()` but takes `&[u8]` instead of a file path.
1552    /// Runs coverage analysis and stores program data for debuggable SVM reloading.
1553    pub fn add_program_from_bytes(
1554        &mut self,
1555        program_id: &Pubkey,
1556        program_data: &[u8],
1557    ) -> Result<()> {
1558        // Honor FUZZ_PROGRAM_SO override even when the caller supplies raw
1559        // bytes (e.g. `rpc_clone` cloning an executable program from mainnet).
1560        // Without this, `--program-so` silently does nothing for rpc-cloned
1561        // programs and coverage replay ends up executing the wrong binary.
1562        let override_bytes;
1563        let program_data: &[u8] = if let Ok(override_path) = std::env::var("FUZZ_PROGRAM_SO") {
1564            eprintln!(
1565                "[COVERAGE] Program binary override: <{} bytes> -> {}",
1566                program_data.len(),
1567                override_path
1568            );
1569            override_bytes = std::fs::read(&override_path).with_context(|| {
1570                format!("failed to read FUZZ_PROGRAM_SO override at {override_path}")
1571            })?;
1572            &override_bytes
1573        } else {
1574            program_data
1575        };
1576
1577        // Run static analysis to get total edge and instruction count for coverage percentages
1578        if let Some((total_edges, total_instructions)) =
1579            Self::analyze_program_coverage(program_data)
1580        {
1581            Arc::make_mut(&mut self.program_coverage_totals)
1582                .insert(*program_id, (total_edges, total_instructions));
1583        }
1584
1585        self.svm
1586            .add_program(program_id.clone(), program_data)
1587            .map_err(|e| anyhow::anyhow!("failed to load program {}: {:?}", program_id, e))?;
1588        // Store program data for reloading into debuggable SVMs
1589        std::sync::Arc::make_mut(&mut self.programs).push(ProgramData {
1590            program_id: *program_id,
1591            data: program_data.to_vec(),
1592        });
1593        Ok(())
1594    }
1595
1596    /// Create an `AccountCloner` for fetching accounts from a Solana RPC endpoint.
1597    ///
1598    /// Accounts are cached to `.fuzz-cache/accounts/` by default.
1599    /// Requires the `rpc-clone` feature.
1600    #[cfg(feature = "rpc-clone")]
1601    pub fn clone_from_rpc(&mut self, rpc_url: &str) -> AccountCloner<'_> {
1602        AccountCloner::new(self, rpc_url)
1603    }
1604
1605    pub fn from_svm(svm: LiteSVM) -> Self {
1606        Self {
1607            svm,
1608            pending_instructions: Vec::new(),
1609            pending_signers: Vec::new(),
1610            programs: std::sync::Arc::new(Vec::new()),
1611            tracked_accounts: Arc::new(HashSet::new()),
1612            program_coverage_totals: Arc::new(HashMap::new()),
1613            snapshot: None,
1614            dirty_tracker: snapshot::DirtyTracker::new(),
1615            sigverify: false,
1616        }
1617    }
1618
1619    pub fn into_svm(self) -> LiteSVM {
1620        self.svm
1621    }
1622
1623    /// Clone this context and set an invocation callback for coverage tracking.
1624    /// The source SVM must have been created with debuggable mode (via CRUCIBLE_FUZZ_DEBUGGABLE env var)
1625    /// for register tracing to work. Cloning preserves the debuggable state and loaded programs.
1626    ///
1627    /// NOTE: For better performance in fuzzing loops, prefer using `set_invocation_callback` after
1628    /// cloning the fixture instead of this method. This method performs an additional SVM clone
1629    /// beyond the fixture clone, which can be expensive.
1630    pub fn clone_with_invocation_callback<C: InvocationInspectCallback + 'static>(
1631        &self,
1632        callback: C,
1633    ) -> Self {
1634        // Just clone the SVM directly and set callback - don't use builder methods
1635        // as they may create a fresh SVM and lose account data
1636        let mut cloned_svm = self.svm.clone();
1637        cloned_svm.set_invocation_inspect_callback(callback);
1638
1639        Self {
1640            svm: cloned_svm,
1641            pending_instructions: self.pending_instructions.clone(),
1642            pending_signers: self
1643                .pending_signers
1644                .iter()
1645                .map(|k| k.insecure_clone())
1646                .collect(),
1647            programs: self.programs.clone(),
1648            tracked_accounts: self.tracked_accounts.clone(),
1649            program_coverage_totals: self.program_coverage_totals.clone(),
1650            snapshot: None,
1651            dirty_tracker: snapshot::DirtyTracker::new(),
1652            sigverify: false,
1653        }
1654    }
1655
1656    /// Set an invocation callback for coverage tracking on this context.
1657    /// Unlike `clone_with_invocation_callback`, this modifies the context in place
1658    /// without performing an additional SVM clone.
1659    ///
1660    /// The SVM must have been created with debuggable mode (via CRUCIBLE_FUZZ_DEBUGGABLE env var)
1661    /// for register tracing to work.
1662    ///
1663    /// Usage pattern for fuzzing loops:
1664    /// ```ignore
1665    /// let mut fixture = template_fixture.clone();  // Single clone
1666    /// fixture.ctx.set_invocation_callback(callback);  // No additional clone
1667    /// ```
1668    pub fn set_invocation_callback<C: InvocationInspectCallback + 'static>(&mut self, callback: C) {
1669        self.svm.set_invocation_inspect_callback(callback);
1670    }
1671
1672    /// Track an account pubkey so it gets copied when cloning with invocation callback.
1673    /// Called internally by account builders.
1674    pub fn track_account(&mut self, pubkey: Pubkey) {
1675        Arc::make_mut(&mut self.tracked_accounts).insert(pubkey);
1676    }
1677
1678    /// Get count of tracked accounts (for debugging)
1679    pub fn tracked_accounts_count(&self) -> usize {
1680        self.tracked_accounts.len()
1681    }
1682
1683    /// Get count of loaded programs (for debugging)
1684    pub fn programs_count(&self) -> usize {
1685        self.programs.len()
1686    }
1687
1688    /// Get total number of accounts in the SVM's internal HashMap.
1689    /// Used to detect unbounded account growth across fuzzing iterations.
1690    pub fn svm_account_count(&self) -> usize {
1691        self.svm.accounts_db().inner.len()
1692    }
1693
1694    /// Check if a specific account exists in the SVM (for debugging)
1695    pub fn account_exists(&self, pubkey: &Pubkey) -> bool {
1696        self.svm.get_account(pubkey).is_some()
1697    }
1698
1699    /// Get the total CFG edge and instruction counts for all loaded programs.
1700    /// Returns HashMap<Pubkey, (total_edges, total_instructions)>
1701    /// Used by the fuzzer to calculate coverage percentages.
1702    pub fn get_program_coverage_totals(&self) -> &HashMap<Pubkey, (usize, usize)> {
1703        &self.program_coverage_totals
1704    }
1705
1706    /// Get program binaries for CFG analysis.
1707    /// Returns a map from program pubkey to binary data.
1708    /// Used by the fuzzer for per-instruction CFG divergence analysis.
1709    pub fn get_program_binaries(&self) -> HashMap<Pubkey, Vec<u8>> {
1710        self.programs
1711            .iter()
1712            .map(|p| (p.program_id, p.data.clone()))
1713            .collect()
1714    }
1715
1716    /// Get program binary by pubkey (for CFG analysis).
1717    pub fn get_program_binary(&self, pubkey: &Pubkey) -> Option<&[u8]> {
1718        self.programs
1719            .iter()
1720            .find(|p| &p.program_id == pubkey)
1721            .map(|p| p.data.as_slice())
1722    }
1723
1724    // =========================================================================
1725    // Snapshot/Restore API (always-on in fuzz mode)
1726    // =========================================================================
1727
1728    /// Take a snapshot of ALL accounts in the SVM.
1729    /// Called once after setup, before the fuzz loop begins.
1730    pub fn take_snapshot(&mut self) {
1731        // Sync tracked_accounts with everything in the SVM so that
1732        // tracked_accounts_count() reflects reality (used in diagnostics).
1733        let db_keys: Vec<Pubkey> = self.svm.accounts_db().inner.keys().copied().collect();
1734        {
1735            let tracked = Arc::make_mut(&mut self.tracked_accounts);
1736            for pubkey in &db_keys {
1737                tracked.insert(*pubkey);
1738            }
1739        }
1740        // Snapshot ALL accounts — not just tracked/dirty ones.
1741        // This is the same approach used by stateful multicore mode (take_all)
1742        // and costs only a one-time O(all_accounts) at setup. Per-iteration
1743        // restore remains O(dirty) since it only touches dirty_tracker accounts.
1744        self.snapshot = Some(snapshot::SvmSnapshot::take_all(&self.svm));
1745        // Clear the dirty tracker so it's fresh for the first iteration
1746        self.dirty_tracker.clear();
1747    }
1748
1749    /// Prepare for a new iteration: clear dirty tracker and pending state.
1750    /// Called at the start of each fuzzing iteration.
1751    pub fn begin_iteration(&mut self) {
1752        self.dirty_tracker.clear();
1753        // Clear pending instructions/signers from previous iteration
1754        self.pending_instructions.clear();
1755        self.pending_signers.clear();
1756    }
1757
1758    /// Restore only dirty accounts from snapshot. Returns count restored.
1759    /// Much faster than full SVM clone when only ~5-20 accounts were modified.
1760    pub fn restore_snapshot(&mut self) -> usize {
1761        if let Some(ref snap) = self.snapshot {
1762            snap.restore(&mut self.svm, &self.dirty_tracker)
1763        } else {
1764            0
1765        }
1766    }
1767
1768    /// Whether a snapshot has been taken.
1769    pub fn has_snapshot(&self) -> bool {
1770        self.snapshot.is_some()
1771    }
1772
1773    /// Get the dirty tracker for the current iteration.
1774    pub fn dirty_tracker(&self) -> &snapshot::DirtyTracker {
1775        &self.dirty_tracker
1776    }
1777
1778    /// Account Creation Helpers
1779
1780    // Create a basic default account
1781    pub fn create_account(&mut self) -> GenericAccountBuilder<'_> {
1782        GenericAccountBuilder {
1783            ctx: self,
1784            address: Pubkey::default(),
1785            account_state: Account {
1786                lamports: 0,
1787                data: vec![],
1788                owner: system_program::id(),
1789                executable: false,
1790                rent_epoch: 0,
1791            },
1792        }
1793    }
1794
1795    // Create a mint account
1796    pub fn create_mint(&mut self) -> MintAccountBuilder<'_> {
1797        let rent = Rent::default();
1798        MintAccountBuilder {
1799            ctx: self,
1800            address: Pubkey::default(),
1801            account_state: Account {
1802                lamports: rent.minimum_balance(spl_token::state::Mint::LEN),
1803                data: vec![0; spl_token::state::Mint::LEN],
1804                owner: spl_token::id(),
1805                executable: false,
1806                rent_epoch: 0,
1807            },
1808            mint: spl_token::state::Mint {
1809                mint_authority: COption::None,
1810                supply: 0,
1811                decimals: 0,
1812                is_initialized: true,
1813                freeze_authority: COption::None,
1814            },
1815        }
1816    }
1817    pub fn create_token_account(&mut self) -> TokenAccountBuilder<'_> {
1818        let rent = Rent::default();
1819        TokenAccountBuilder {
1820            ctx: self,
1821            address: Pubkey::default(),
1822            account_state: Account {
1823                lamports: rent.minimum_balance(spl_token::state::Account::LEN),
1824                data: vec![0; spl_token::state::Account::LEN],
1825                owner: spl_token::id(),
1826                executable: false,
1827                rent_epoch: 0,
1828            },
1829            token_state: spl_token::state::Account {
1830                mint: Pubkey::default(),
1831                owner: Pubkey::default(),
1832                amount: 0,
1833                delegate: COption::None,
1834                state: spl_token::state::AccountState::Initialized,
1835                is_native: COption::None,
1836                delegated_amount: 0,
1837                close_authority: COption::None,
1838            },
1839        }
1840    }
1841
1842    /// Transfer tokens between accounts
1843    pub fn transfer_tokens(
1844        &mut self,
1845        from: &Pubkey,
1846        to: &Pubkey,
1847        owner: &Keypair,
1848        amount: u64,
1849    ) -> anyhow::Result<()> {
1850        self.raw_call(spl_token::instruction::transfer(
1851            &spl_token::id(),
1852            from,
1853            to,
1854            &owner.pubkey(),
1855            &[],
1856            amount,
1857        )?)
1858        .signers(&[owner])
1859        .send()?;
1860        Ok(())
1861    }
1862
1863    pub fn mint_to(
1864        &mut self,
1865        mint: &Pubkey,
1866        destination: &Pubkey,
1867        amount: u64,
1868        authority: &Rc<Keypair>,
1869    ) -> anyhow::Result<()> {
1870        self.raw_call(spl_token::instruction::mint_to(
1871            &spl_token::id(),
1872            mint,
1873            destination,
1874            &authority.pubkey(),
1875            &[],
1876            amount,
1877        )?)
1878        .signers(&[&**authority])
1879        .send()?;
1880        Ok(())
1881    }
1882
1883    pub fn warp_to_slot(&mut self, slot: u64) {
1884        self.dirty_tracker.mark_clock_dirty(slot);
1885        self.svm.warp_to_slot(slot);
1886    }
1887
1888    pub fn advance_slots(&mut self, slots: u64) {
1889        let current_slot = self.slot();
1890        let target_slot = current_slot + slots;
1891        self.dirty_tracker.mark_clock_dirty(target_slot);
1892        self.svm.warp_to_slot(target_slot);
1893    }
1894
1895    pub fn set_sysvar<T>(&mut self, sysvar: &T) -> ()
1896    where
1897        T: solana_sysvar::SysvarSerialize,
1898    {
1899        self.svm.set_sysvar::<T>(sysvar);
1900    }
1901
1902    /// Getters
1903
1904    pub fn slot(&self) -> u64 {
1905        self.svm.get_sysvar::<Clock>().slot
1906    }
1907
1908    /// Returns the slot that the next transaction will likely see (current + 1)
1909    pub fn next_slot(&self) -> u64 {
1910        self.slot() + 1
1911    }
1912
1913    // =========================================================================
1914    // Account Read/Write API
1915    //
1916    // **Raw Account** (any format):
1917    //   - `read_account` / `get_account` — returns raw `Account`
1918    //   - `write_account` — sets raw `Account`
1919    //   - `update_account` — closure-based read-modify-write on raw bytes
1920    //
1921    // **Anchor / Borsh** (`#[account]` structs):
1922    //   - `read_anchor_account<T>` — auto discriminator via `T::DISCRIMINATOR`
1923    //   - `read_account_with_discriminator<T>` — explicit discriminator length
1924    //   - `write_anchor_account<T>` — serializes with discriminator
1925    //
1926    // **Zero-Copy / Bytemuck** (`#[account(zero_copy)]` structs):
1927    //   - `read_zero_copy_account<T>` — 8-byte discriminator (standard Anchor)
1928    //   - `read_zero_copy_account_with_discriminator<T>` — explicit discriminator length
1929    //   - `write_zero_copy_account<T>` — preserves 8-byte discriminator
1930    //   - `write_zero_copy_account_with_discriminator<T>` — explicit discriminator length
1931    //
1932    // **Utilities:**
1933    //   - `account_has_data` — existence + minimum size check
1934    //   - `token_balance` — SPL token amount shorthand
1935    // =========================================================================
1936
1937    /// Check if account exists AND has at least `min_size` bytes of data
1938    pub fn account_has_data(&self, pubkey: &Pubkey, min_size: usize) -> bool {
1939        self.svm
1940            .get_account(pubkey)
1941            .map(|acc| acc.data.len() >= min_size)
1942            .unwrap_or(false)
1943    }
1944
1945    pub fn get_account(&self, address: &Pubkey) -> Result<Account> {
1946        self.read_account(address)
1947    }
1948
1949    // Read an account at a Pubkey
1950    pub fn read_account(&self, address: &Pubkey) -> Result<Account> {
1951        self.svm
1952            .get_account(address)
1953            .ok_or_else(|| anyhow::anyhow!("Account not found: {}", address))
1954    }
1955
1956    /// Read anchor account at address and deserialize the data.
1957    /// Uses the type's DISCRIMINATOR to determine how many bytes to skip.
1958    pub fn read_anchor_account<T: AnchorDeserialize + Discriminator>(
1959        &self,
1960        address: &Pubkey,
1961    ) -> Result<T> {
1962        let account = self.read_account(address)?;
1963        let disc_len = T::DISCRIMINATOR.len();
1964
1965        if account.data.len() < disc_len {
1966            return Err(anyhow::anyhow!(
1967                "Account data too small for discriminator (need {} bytes, got {})",
1968                disc_len,
1969                account.data.len()
1970            ));
1971        }
1972
1973        // Deserialize from bytes after discriminator
1974        T::deserialize(&mut &account.data[disc_len..])
1975            .map_err(|e| anyhow::anyhow!("Failed to deserialize account: {}", e))
1976    }
1977
1978    /// Read account with explicit discriminator length (for non-standard accounts).
1979    pub fn read_account_with_discriminator<T: AnchorDeserialize>(
1980        &self,
1981        address: &Pubkey,
1982        discriminator_len: usize,
1983    ) -> Result<T> {
1984        let account = self.read_account(address)?;
1985
1986        if account.data.len() < discriminator_len {
1987            return Err(anyhow::anyhow!(
1988                "Account data too small for discriminator (need {} bytes, got {})",
1989                discriminator_len,
1990                account.data.len()
1991            ));
1992        }
1993
1994        T::deserialize(&mut &account.data[discriminator_len..])
1995            .map_err(|e| anyhow::anyhow!("Failed to deserialize account: {}", e))
1996    }
1997
1998    pub fn token_balance(&self, token_account: &Pubkey) -> u64 {
1999        self.svm
2000            .get_account(token_account)
2001            .and_then(|acc| spl_token::state::Account::unpack(&acc.data).ok())
2002            .map(|state| state.amount)
2003            .unwrap_or(0)
2004    }
2005
2006    /// Setters
2007
2008    // Write account directly to SVM
2009    pub fn write_account(&mut self, address: &Pubkey, account: Account) -> Result<()> {
2010        Arc::make_mut(&mut self.tracked_accounts).insert(*address);
2011        self.dirty_tracker.mark_account_dirty(address);
2012        let _ = self.svm.set_account(*address, account);
2013        Ok(())
2014    }
2015
2016    // Serialize with discriminator, write to SVM
2017    pub fn write_anchor_account<T: AnchorSerialize + Discriminator>(
2018        &mut self,
2019        address: &Pubkey,
2020        data: &T,
2021    ) -> Result<()> {
2022        // Read existing account to preserve lamports, owner, etc.
2023        let mut account = self.read_account(address)?;
2024
2025        // Build new data: discriminator + serialized T
2026        let mut account_data = T::DISCRIMINATOR.to_vec();
2027        data.serialize(&mut account_data)?;
2028
2029        // Update account data and write back
2030        account.data = account_data;
2031        self.dirty_tracker.mark_account_dirty(address);
2032        let _ = self.svm.set_account(*address, account);
2033
2034        Ok(())
2035    }
2036
2037    /// Read a zero-copy account (skips 8-byte discriminator).
2038    ///
2039    /// Read a zero-copy account with standard 8-byte discriminator.
2040    ///
2041    /// Use this for accounts with `#[account(zero_copy)]` attribute which use
2042    /// bytemuck for serialization instead of Borsh.
2043    ///
2044    /// # Example
2045    /// ```ignore
2046    /// let reserve: Reserve = ctx.read_zero_copy_account(&reserve_addr)?;
2047    /// println!("Reserve slot: {}", reserve.last_update.slot);
2048    /// ```
2049    pub fn read_zero_copy_account<T: bytemuck::Pod>(&self, address: &Pubkey) -> Result<T> {
2050        self.read_zero_copy_account_with_discriminator(address, 8)
2051    }
2052
2053    /// Read a zero-copy account with explicit discriminator length.
2054    ///
2055    /// Use this for accounts with non-standard discriminator sizes.
2056    ///
2057    /// # Example
2058    /// ```ignore
2059    /// // For accounts with 1-byte discriminator
2060    /// let vault: StarVault = ctx.read_zero_copy_account_with_discriminator(&vault_addr, 1)?;
2061    /// ```
2062    pub fn read_zero_copy_account_with_discriminator<T: bytemuck::Pod>(
2063        &self,
2064        address: &Pubkey,
2065        discriminator_len: usize,
2066    ) -> Result<T> {
2067        let account = self.read_account(address)?;
2068        let required_size = discriminator_len + std::mem::size_of::<T>();
2069        if account.data.len() < required_size {
2070            return Err(anyhow::anyhow!(
2071                "Account data too small for zero-copy struct: got {} bytes, need {} bytes (discriminator: {})",
2072                account.data.len(),
2073                required_size,
2074                discriminator_len
2075            ));
2076        }
2077        Ok(*bytemuck::from_bytes::<T>(
2078            &account.data[discriminator_len..discriminator_len + std::mem::size_of::<T>()],
2079        ))
2080    }
2081
2082    /// Write a zero-copy account (preserves 8-byte discriminator).
2083    ///
2084    /// Use this for accounts with `#[account(zero_copy)]` attribute which use
2085    /// bytemuck for serialization instead of Borsh.
2086    ///
2087    /// # Example
2088    /// ```ignore
2089    /// let mut reserve: Reserve = ctx.read_zero_copy_account(&reserve_addr)?;
2090    /// reserve.last_update.mark_fresh(current_slot);
2091    /// ctx.write_zero_copy_account(&reserve_addr, &reserve)?;
2092    /// ```
2093    pub fn write_zero_copy_account<T: bytemuck::Pod>(
2094        &mut self,
2095        address: &Pubkey,
2096        data: &T,
2097    ) -> Result<()> {
2098        self.write_zero_copy_account_with_discriminator(address, data, 8)
2099    }
2100
2101    /// Write a zero-copy account with explicit discriminator length.
2102    pub fn write_zero_copy_account_with_discriminator<T: bytemuck::Pod>(
2103        &mut self,
2104        address: &Pubkey,
2105        data: &T,
2106        discriminator_len: usize,
2107    ) -> Result<()> {
2108        let mut account = self.read_account(address)?;
2109        let bytes = bytemuck::bytes_of(data);
2110        let required_size = discriminator_len + bytes.len();
2111        if account.data.len() < required_size {
2112            return Err(anyhow::anyhow!(
2113                "Account data too small for zero-copy struct: got {} bytes, need {} bytes",
2114                account.data.len(),
2115                required_size
2116            ));
2117        }
2118        account.data[discriminator_len..discriminator_len + bytes.len()].copy_from_slice(bytes);
2119        Arc::make_mut(&mut self.tracked_accounts).insert(*address);
2120        self.dirty_tracker.mark_account_dirty(address);
2121        let _ = self.svm.set_account(*address, account);
2122        Ok(())
2123    }
2124
2125    /// Update account data using a closure. Enables atomic read-modify-write pattern.
2126    ///
2127    /// # Example
2128    /// ```ignore
2129    /// ctx.update_account(&reserve_pubkey, |data| {
2130    ///     // Modify data in place (e.g., using bytemuck)
2131    ///     let reserve: &mut Reserve = bytemuck::from_bytes_mut(&mut data[8..]);
2132    ///     reserve.config.loan_to_value_pct = 80;
2133    /// })?;
2134    /// ```
2135    pub fn update_account<F>(&mut self, pubkey: &Pubkey, f: F) -> Result<()>
2136    where
2137        F: FnOnce(&mut Vec<u8>),
2138    {
2139        let mut account = self.read_account(pubkey)?;
2140        f(&mut account.data);
2141        self.write_account(pubkey, account)
2142    }
2143
2144    /// Callers - each returns a builder
2145
2146    // Escape hatch for raw instructions
2147    pub fn raw_call(&mut self, instruction: Instruction) -> InstructionBuilder<'_> {
2148        InstructionBuilder {
2149            ctx: self,
2150            instruction,
2151            signers: vec![],
2152            fee_payer: None,
2153        }
2154    }
2155
2156    // For calling Anchor programs dynamically
2157    pub fn program(&mut self, program_id: Pubkey) -> ProgramBuilder<'_> {
2158        ProgramBuilder {
2159            ctx: self,
2160            instruction: Instruction {
2161                program_id,
2162                accounts: vec![],
2163                data: vec![],
2164            },
2165            signers: vec![],
2166            fee_payer: None, // Use first signer by default
2167        }
2168    }
2169
2170    // For batching multiple instructions
2171    pub fn transaction(&mut self) -> TransactionBuilder<'_> {
2172        TransactionBuilder {
2173            ctx: self,
2174            instructions: vec![],
2175            signers: vec![],
2176        }
2177    }
2178
2179    pub fn send_batch(&mut self) -> Result<Option<TxOutcome>> {
2180        // Empty queue is a noop
2181        if self.pending_instructions.is_empty() {
2182            return Ok(None);
2183        }
2184
2185        let debug = is_fuzz_debug();
2186        let num_ixs = self.pending_instructions.len();
2187
2188        // Deduplicate signers while preserving order (first = fee payer)
2189        let mut seen = std::collections::HashSet::new();
2190        let unique_signers: Vec<&Keypair> = self
2191            .pending_signers
2192            .iter()
2193            .filter(|k| seen.insert(k.pubkey()))
2194            .collect();
2195
2196        let fee_payer_pubkey = unique_signers
2197            .first()
2198            .map(|k| k.pubkey())
2199            .unwrap_or_default();
2200
2201        let default_kp = Keypair::new();
2202
2203        let fee_payer = unique_signers.first().map(|k| *k).ok_or(anyhow::anyhow!(
2204            "At least one signer required for send_batch. The first signer is the fee payer."
2205        ))?;
2206
2207        if debug {
2208            eprintln!("[TX] Sending batch with {} instructions", num_ixs);
2209            for (i, ix) in self.pending_instructions.iter().enumerate() {
2210                eprintln!("[TX]   ix[{}]: program={}", i, ix.program_id);
2211            }
2212        }
2213
2214        // Pre-tx: dirty tracking
2215        let __t_pre = std::time::Instant::now();
2216        self.dirty_tracker
2217            .record_tx(&self.pending_instructions, &fee_payer_pubkey);
2218        SEND_BATCH_PRE_NS.with(|c| c.set(c.get() + __t_pre.elapsed().as_nanos() as u64));
2219
2220        // SVM execution: send transaction
2221        let __t_svm = std::time::Instant::now();
2222        let instructions = std::mem::take(&mut self.pending_instructions);
2223        let result = instruction_builder::send_transaction(
2224            &mut self.svm,
2225            instructions,
2226            &unique_signers,
2227            fee_payer,
2228            self.sigverify,
2229        )?;
2230        SEND_BATCH_SVM_NS.with(|c| c.set(c.get() + __t_svm.elapsed().as_nanos() as u64));
2231
2232        // Post-tx: outcome parsing
2233        let __t_post = std::time::Instant::now();
2234        let outcome = tx_result_to_outcome(result);
2235
2236        // Track tx success/failure for monitor display (works for all modes)
2237        increment_action_count();
2238        if outcome.is_success() {
2239            increment_action_success_count();
2240        }
2241
2242        if debug {
2243            match &outcome {
2244                TxOutcome::Success {
2245                    compute_units,
2246                    logs,
2247                    ..
2248                } => {
2249                    eprintln!("[TX] SUCCESS - compute_units={}, logs:", compute_units);
2250                    for log in logs {
2251                        eprintln!("[TX]   {}", log);
2252                    }
2253                }
2254                TxOutcome::ProgramError {
2255                    error,
2256                    error_code,
2257                    logs,
2258                    ..
2259                } => {
2260                    eprintln!("[TX] FAILED - error: {:?}", error);
2261                    if let Some(code) = error_code {
2262                        eprintln!("[TX]   error code: {}", code);
2263                    }
2264                    eprintln!("[TX]   logs:");
2265                    for log in logs {
2266                        eprintln!("[TX]   {}", log);
2267                    }
2268                }
2269            }
2270        }
2271        SEND_BATCH_POST_NS.with(|c| c.set(c.get() + __t_post.elapsed().as_nanos() as u64));
2272
2273        // Clear signers queue (pending_instructions already taken via std::mem::take)
2274        self.pending_signers.clear();
2275
2276        Ok(Some(outcome))
2277    }
2278}
2279
2280// ============================================================================
2281// Unit Tests
2282// ============================================================================
2283
2284#[cfg(test)]
2285mod tests {
2286    use super::*;
2287
2288    // -------------------------------------------------------------------------
2289    // Violation tracking tests
2290    // -------------------------------------------------------------------------
2291
2292    #[test]
2293    fn test_violation_tracking_basic() {
2294        // Clear any existing violation
2295        let _ = take_violation();
2296
2297        // No violation initially
2298        assert!(!has_violation());
2299        assert!(take_violation().is_none());
2300
2301        // Record a violation
2302        record_violation("test violation".to_string());
2303
2304        // Violation should be recorded
2305        assert!(has_violation());
2306
2307        // Taking the violation should return it and clear it
2308        let v = take_violation();
2309        assert_eq!(v, Some("test violation".to_string()));
2310        assert!(!has_violation());
2311        assert!(take_violation().is_none());
2312    }
2313
2314    #[test]
2315    fn test_violation_only_records_first() {
2316        let _ = take_violation();
2317
2318        record_violation("first".to_string());
2319        record_violation("second".to_string());
2320        record_violation("third".to_string());
2321
2322        // Only the first violation should be recorded
2323        let v = take_violation();
2324        assert_eq!(v, Some("first".to_string()));
2325    }
2326
2327    // -------------------------------------------------------------------------
2328    // Action history tests
2329    // -------------------------------------------------------------------------
2330
2331    #[test]
2332    fn test_action_history() {
2333        clear_action_history();
2334
2335        assert!(get_action_history().is_empty());
2336
2337        push_action_record("action_deposit", serde_json::json!({"amount": 100}), true);
2338        push_action_record("action_withdraw", serde_json::json!({"amount": 50}), false);
2339
2340        let history = get_action_history();
2341        assert_eq!(history.len(), 2);
2342        assert_eq!(history[0].name, "action_deposit");
2343        assert!(history[0].success);
2344        assert_eq!(history[0].error_code, None);
2345        assert_eq!(history[1].name, "action_withdraw");
2346        assert!(!history[1].success);
2347        assert_eq!(history[1].error_code, None);
2348
2349        // Test with error code passthrough
2350        clear_action_history();
2351        set_last_error_code(Some(6051));
2352        push_action_record("action_borrow", serde_json::json!({}), false);
2353        let history = get_action_history();
2354        assert_eq!(history[0].error_code, Some(6051));
2355        // Verify TLS is cleared after take
2356        push_action_record("action_deposit", serde_json::json!({}), true);
2357        let history = get_action_history();
2358        assert_eq!(history[1].error_code, None);
2359
2360        clear_action_history();
2361        assert!(get_action_history().is_empty());
2362    }
2363
2364    // -------------------------------------------------------------------------
2365    // -------------------------------------------------------------------------
2366    // Violation action index tests
2367    // -------------------------------------------------------------------------
2368
2369    #[test]
2370    fn test_violation_action_index() {
2371        clear_violation_tracking();
2372
2373        assert!(get_violation_action_index().is_none());
2374
2375        set_total_actions(5);
2376        set_violation_action_index(2);
2377
2378        assert_eq!(get_violation_action_index(), Some(2));
2379
2380        // Only records first violation index
2381        set_violation_action_index(3);
2382        assert_eq!(get_violation_action_index(), Some(2));
2383
2384        clear_violation_tracking();
2385        assert!(get_violation_action_index().is_none());
2386    }
2387
2388    // -------------------------------------------------------------------------
2389    // IntoActionSuccess trait tests
2390    // -------------------------------------------------------------------------
2391
2392    #[test]
2393    fn test_into_action_success_unit() {
2394        assert!(().into_success());
2395    }
2396
2397    #[test]
2398    fn test_into_action_success_result() {
2399        let ok: Result<(), &str> = Ok(());
2400        let err: Result<(), &str> = Err("error");
2401
2402        assert!(ok.into_success());
2403        assert!(!err.into_success());
2404    }
2405
2406    #[test]
2407    fn test_into_action_success_bool() {
2408        assert!(true.into_success());
2409        assert!(!false.into_success());
2410    }
2411
2412    // -------------------------------------------------------------------------
2413    // Iteration tracking tests
2414    // -------------------------------------------------------------------------
2415
2416    #[test]
2417    fn test_iteration_tracking() {
2418        set_current_iteration(42);
2419        assert_eq!(get_current_iteration(), 42);
2420
2421        set_current_iteration(100);
2422        assert_eq!(get_current_iteration(), 100);
2423    }
2424
2425    #[test]
2426    fn test_test_name_tracking() {
2427        set_current_test_name("my_test");
2428        assert_eq!(get_current_test_name(), Some("my_test".to_string()));
2429    }
2430
2431    // -------------------------------------------------------------------------
2432    // Crash metadata tests
2433    // -------------------------------------------------------------------------
2434
2435    #[test]
2436    fn test_build_crash_metadata() {
2437        clear_action_history();
2438        set_current_test_name("test_func");
2439        set_current_iteration(999);
2440
2441        push_action_record("action_a", serde_json::json!({"x": 1}), true);
2442
2443        let meta = build_crash_metadata(Some(12345));
2444
2445        assert_eq!(meta.test_name, "test_func");
2446        assert_eq!(meta.iteration, 999);
2447        assert_eq!(meta.seed, Some(12345));
2448        assert_eq!(meta.actions.len(), 1);
2449        assert_eq!(meta.actions[0].name, "action_a");
2450        assert_eq!(meta.actions[0].error_code, None);
2451    }
2452
2453    // -------------------------------------------------------------------------
2454    // TxOutcome tests
2455    // -------------------------------------------------------------------------
2456
2457    #[test]
2458    fn test_tx_outcome_helpers() {
2459        let success = TxOutcome::Success {
2460            compute_units: 100,
2461            logs: vec!["log1".to_string()],
2462            signature: Signature::default(),
2463            inner_instructions: vec![],
2464            return_data: TransactionReturnData::default(),
2465            fee: 5000,
2466        };
2467
2468        assert!(success.is_success());
2469        assert!(!success.is_error());
2470        assert!(success.error_code().is_none());
2471        assert_eq!(success.compute_units(), Some(100));
2472        assert_eq!(success.logs().len(), 1);
2473        assert_eq!(success.fee(), 5000);
2474
2475        let error = TxOutcome::ProgramError {
2476            error: TransactionError::AccountInUse,
2477            error_code: Some(6051),
2478            instruction_index: Some(0),
2479            logs: vec!["error log".to_string()],
2480            signature: Signature::default(),
2481            inner_instructions: vec![],
2482            return_data: TransactionReturnData::default(),
2483            fee: 0,
2484        };
2485
2486        assert!(!error.is_success());
2487        assert!(error.is_error());
2488        assert_eq!(error.error_code(), Some(6051));
2489        assert!(error.compute_units().is_none());
2490        assert_eq!(error.fee(), 0);
2491    }
2492
2493    #[test]
2494    fn test_tx_outcome_into_result() {
2495        let success = TxOutcome::Success {
2496            compute_units: 100,
2497            logs: vec![],
2498            signature: Signature::default(),
2499            inner_instructions: vec![],
2500            return_data: TransactionReturnData::default(),
2501            fee: 0,
2502        };
2503        assert!(success.into_result().is_ok());
2504
2505        let error = TxOutcome::ProgramError {
2506            error: TransactionError::AccountInUse,
2507            error_code: Some(6051),
2508            instruction_index: Some(0),
2509            logs: vec![],
2510            signature: Signature::default(),
2511            inner_instructions: vec![],
2512            return_data: TransactionReturnData::default(),
2513            fee: 0,
2514        };
2515        let err = error.into_result().unwrap_err();
2516        assert_eq!(err.error_code, Some(6051));
2517        assert_eq!(err.fee, 0);
2518    }
2519
2520    // -------------------------------------------------------------------------
2521    // format_json_value tests
2522    // -------------------------------------------------------------------------
2523
2524    #[test]
2525    fn test_format_json_value() {
2526        assert_eq!(format_json_value(&serde_json::json!(null)), "null");
2527        assert_eq!(format_json_value(&serde_json::json!(true)), "true");
2528        assert_eq!(format_json_value(&serde_json::json!(42)), "42");
2529        assert_eq!(format_json_value(&serde_json::json!("hello")), "\"hello\"");
2530        assert_eq!(
2531            format_json_value(&serde_json::json!([1, 2, 3])),
2532            "[1, 2, 3]"
2533        );
2534        assert_eq!(format_json_value(&serde_json::json!({"a": 1})), "{a: 1}");
2535    }
2536
2537    // -------------------------------------------------------------------------
2538    // Snapshot/Restore integration tests
2539    // -------------------------------------------------------------------------
2540
2541    #[test]
2542    fn test_snapshot_basic_roundtrip() {
2543        let mut ctx = TestContext::new();
2544
2545        // Write some accounts
2546        let pk1 = Pubkey::new_unique();
2547        let pk2 = Pubkey::new_unique();
2548        let owner = Pubkey::new_unique();
2549
2550        ctx.write_account(
2551            &pk1,
2552            Account {
2553                lamports: 1_000_000,
2554                data: vec![1, 2, 3, 4],
2555                owner,
2556                executable: false,
2557                rent_epoch: 0,
2558            },
2559        )
2560        .unwrap();
2561
2562        ctx.write_account(
2563            &pk2,
2564            Account {
2565                lamports: 2_000_000,
2566                data: vec![10, 20, 30],
2567                owner,
2568                executable: false,
2569                rent_epoch: 0,
2570            },
2571        )
2572        .unwrap();
2573
2574        // Take snapshot
2575        ctx.take_snapshot();
2576        assert!(ctx.has_snapshot());
2577
2578        // Begin iteration (clears dirty tracker)
2579        ctx.begin_iteration();
2580
2581        // Modify accounts
2582        ctx.write_account(
2583            &pk1,
2584            Account {
2585                lamports: 999,
2586                data: vec![99, 99, 99, 99],
2587                owner,
2588                executable: false,
2589                rent_epoch: 0,
2590            },
2591        )
2592        .unwrap();
2593
2594        ctx.write_account(
2595            &pk2,
2596            Account {
2597                lamports: 0,
2598                data: vec![],
2599                owner,
2600                executable: false,
2601                rent_epoch: 0,
2602            },
2603        )
2604        .unwrap();
2605
2606        // Verify accounts are modified
2607        let acc1 = ctx.read_account(&pk1).unwrap();
2608        assert_eq!(acc1.lamports, 999);
2609        assert_eq!(acc1.data, vec![99, 99, 99, 99]);
2610
2611        // Restore snapshot
2612        let restored = ctx.restore_snapshot();
2613        assert_eq!(restored, 2); // 2 dirty accounts
2614
2615        // Verify accounts are back to original state
2616        let acc1 = ctx.read_account(&pk1).unwrap();
2617        assert_eq!(acc1.lamports, 1_000_000);
2618        assert_eq!(acc1.data, vec![1, 2, 3, 4]);
2619
2620        let acc2 = ctx.read_account(&pk2).unwrap();
2621        assert_eq!(acc2.lamports, 2_000_000);
2622        assert_eq!(acc2.data, vec![10, 20, 30]);
2623    }
2624
2625    #[test]
2626    fn test_snapshot_created_account_removed_on_restore() {
2627        let mut ctx = TestContext::new();
2628
2629        // Write one initial account
2630        let pk_initial = Pubkey::new_unique();
2631        let owner = Pubkey::new_unique();
2632        ctx.write_account(
2633            &pk_initial,
2634            Account {
2635                lamports: 1_000_000,
2636                data: vec![1, 2, 3],
2637                owner,
2638                executable: false,
2639                rent_epoch: 0,
2640            },
2641        )
2642        .unwrap();
2643
2644        ctx.take_snapshot();
2645        ctx.begin_iteration();
2646
2647        // Create a new account that didn't exist at snapshot time
2648        let pk_new = Pubkey::new_unique();
2649        ctx.write_account(
2650            &pk_new,
2651            Account {
2652                lamports: 500_000,
2653                data: vec![42],
2654                owner,
2655                executable: false,
2656                rent_epoch: 0,
2657            },
2658        )
2659        .unwrap();
2660
2661        // Verify new account exists
2662        assert!(ctx.read_account(&pk_new).is_ok());
2663
2664        // Restore: new account should be zeroed out
2665        ctx.restore_snapshot();
2666
2667        // Account should now be zeroed (lamports=0 means effectively deleted)
2668        let acc = ctx.svm.get_account(&pk_new);
2669        match acc {
2670            Some(a) => assert_eq!(a.lamports, 0, "Created account should be zeroed on restore"),
2671            None => {} // Also acceptable — SVM may remove zero-lamport accounts
2672        }
2673
2674        // Initial account should still be intact
2675        let acc_initial = ctx.read_account(&pk_initial).unwrap();
2676        assert_eq!(acc_initial.lamports, 1_000_000);
2677    }
2678
2679    #[test]
2680    fn test_snapshot_clock_restore() {
2681        let mut ctx = TestContext::new();
2682
2683        // Set a known slot before snapshot
2684        ctx.warp_to_slot(100);
2685        let original_slot = ctx.slot();
2686        assert_eq!(original_slot, 100);
2687
2688        ctx.take_snapshot();
2689        ctx.begin_iteration();
2690
2691        // Advance the clock
2692        ctx.warp_to_slot(500);
2693        assert_eq!(ctx.slot(), 500);
2694
2695        // Restore snapshot
2696        ctx.restore_snapshot();
2697
2698        // Clock should be back to original
2699        assert_eq!(ctx.slot(), 100);
2700    }
2701
2702    #[test]
2703    fn test_snapshot_multiple_iterations() {
2704        let mut ctx = TestContext::new();
2705
2706        let pk = Pubkey::new_unique();
2707        let owner = Pubkey::new_unique();
2708
2709        ctx.write_account(
2710            &pk,
2711            Account {
2712                lamports: 1_000_000,
2713                data: vec![0; 32],
2714                owner,
2715                executable: false,
2716                rent_epoch: 0,
2717            },
2718        )
2719        .unwrap();
2720
2721        ctx.take_snapshot();
2722
2723        // Simulate multiple fuzzing iterations
2724        for i in 0..5 {
2725            ctx.begin_iteration();
2726
2727            // Each iteration modifies the account differently
2728            ctx.write_account(
2729                &pk,
2730                Account {
2731                    lamports: (i + 1) * 100,
2732                    data: vec![i as u8; 32],
2733                    owner,
2734                    executable: false,
2735                    rent_epoch: 0,
2736                },
2737            )
2738            .unwrap();
2739
2740            // Verify modification took effect
2741            let acc = ctx.read_account(&pk).unwrap();
2742            assert_eq!(acc.lamports, (i + 1) * 100);
2743
2744            // Restore to original
2745            ctx.restore_snapshot();
2746
2747            // Verify restoration
2748            let acc = ctx.read_account(&pk).unwrap();
2749            assert_eq!(acc.lamports, 1_000_000, "Failed on iteration {}", i);
2750            assert_eq!(acc.data, vec![0; 32], "Failed on iteration {}", i);
2751        }
2752    }
2753
2754    #[test]
2755    fn test_snapshot_dirty_tracker_tracks_write_account() {
2756        let mut ctx = TestContext::new();
2757
2758        let pk1 = Pubkey::new_unique();
2759        let pk2 = Pubkey::new_unique();
2760        let owner = Pubkey::new_unique();
2761
2762        ctx.write_account(
2763            &pk1,
2764            Account {
2765                lamports: 100,
2766                data: vec![],
2767                owner,
2768                executable: false,
2769                rent_epoch: 0,
2770            },
2771        )
2772        .unwrap();
2773
2774        ctx.write_account(
2775            &pk2,
2776            Account {
2777                lamports: 200,
2778                data: vec![],
2779                owner,
2780                executable: false,
2781                rent_epoch: 0,
2782            },
2783        )
2784        .unwrap();
2785
2786        // Both should be tracked as dirty
2787        assert!(ctx.dirty_tracker.dirty_accounts().contains(&pk1));
2788        assert!(ctx.dirty_tracker.dirty_accounts().contains(&pk2));
2789        assert_eq!(ctx.dirty_tracker.dirty_count(), 2);
2790
2791        // begin_iteration clears the tracker and pending state
2792        ctx.begin_iteration();
2793        assert_eq!(ctx.dirty_tracker.dirty_count(), 0);
2794        assert!(ctx.pending_instructions.is_empty());
2795    }
2796
2797    #[test]
2798    fn test_snapshot_dirty_tracker_tracks_clock() {
2799        let mut ctx = TestContext::new();
2800
2801        assert!(!ctx.dirty_tracker.is_clock_dirty());
2802
2803        ctx.warp_to_slot(100);
2804        assert!(ctx.dirty_tracker.is_clock_dirty());
2805
2806        ctx.begin_iteration();
2807        assert!(!ctx.dirty_tracker.is_clock_dirty());
2808
2809        ctx.advance_slots(10);
2810        assert!(ctx.dirty_tracker.is_clock_dirty());
2811    }
2812
2813    #[test]
2814    fn test_snapshot_no_snapshot_returns_zero() {
2815        let mut ctx = TestContext::new();
2816
2817        // Without taking a snapshot, restore should return 0
2818        assert!(!ctx.has_snapshot());
2819        let restored = ctx.restore_snapshot();
2820        assert_eq!(restored, 0);
2821    }
2822
2823    #[test]
2824    fn test_snapshot_unmodified_accounts_untouched() {
2825        let mut ctx = TestContext::new();
2826
2827        let pk_modified = Pubkey::new_unique();
2828        let pk_untouched = Pubkey::new_unique();
2829        let owner = Pubkey::new_unique();
2830
2831        ctx.write_account(
2832            &pk_modified,
2833            Account {
2834                lamports: 100,
2835                data: vec![1, 2, 3],
2836                owner,
2837                executable: false,
2838                rent_epoch: 0,
2839            },
2840        )
2841        .unwrap();
2842
2843        ctx.write_account(
2844            &pk_untouched,
2845            Account {
2846                lamports: 200,
2847                data: vec![4, 5, 6],
2848                owner,
2849                executable: false,
2850                rent_epoch: 0,
2851            },
2852        )
2853        .unwrap();
2854
2855        ctx.take_snapshot();
2856        ctx.begin_iteration();
2857
2858        // Only modify one account
2859        ctx.write_account(
2860            &pk_modified,
2861            Account {
2862                lamports: 999,
2863                data: vec![9, 9, 9],
2864                owner,
2865                executable: false,
2866                rent_epoch: 0,
2867            },
2868        )
2869        .unwrap();
2870
2871        // Only 1 dirty account (pk_modified) should be restored
2872        let restored = ctx.restore_snapshot();
2873        assert_eq!(restored, 1);
2874
2875        // Modified account restored
2876        let acc = ctx.read_account(&pk_modified).unwrap();
2877        assert_eq!(acc.lamports, 100);
2878
2879        // Untouched account still has original data
2880        let acc = ctx.read_account(&pk_untouched).unwrap();
2881        assert_eq!(acc.lamports, 200);
2882        assert_eq!(acc.data, vec![4, 5, 6]);
2883    }
2884
2885    #[test]
2886    fn test_snapshot_clone_does_not_inherit_snapshot() {
2887        let mut ctx = TestContext::new();
2888
2889        let pk = Pubkey::new_unique();
2890        ctx.write_account(
2891            &pk,
2892            Account {
2893                lamports: 100,
2894                data: vec![],
2895                owner: Pubkey::new_unique(),
2896                executable: false,
2897                rent_epoch: 0,
2898            },
2899        )
2900        .unwrap();
2901
2902        ctx.take_snapshot();
2903        assert!(ctx.has_snapshot());
2904
2905        // Clone should NOT have a snapshot
2906        let cloned = ctx.clone();
2907        assert!(!cloned.has_snapshot());
2908
2909        // Clone should have a fresh dirty tracker
2910        assert_eq!(cloned.dirty_tracker.dirty_count(), 0);
2911    }
2912
2913    #[test]
2914    fn test_snapshot_includes_dirty_tracker_accounts() {
2915        // Simulates CPI-created accounts: dirty tracker tracks them during setup
2916        // but they're not in tracked_accounts. take_snapshot() should include them.
2917        let mut ctx = TestContext::new();
2918
2919        let pk_tracked = Pubkey::new_unique();
2920        let pk_cpi = Pubkey::new_unique();
2921        let owner = Pubkey::new_unique();
2922
2923        // This goes through write_account → tracked_accounts
2924        ctx.write_account(
2925            &pk_tracked,
2926            Account {
2927                lamports: 100,
2928                data: vec![1],
2929                owner,
2930                executable: false,
2931                rent_epoch: 0,
2932            },
2933        )
2934        .unwrap();
2935
2936        // Simulate a CPI-created account: manually set in SVM and mark dirty
2937        // (In real usage, this happens via record_tx during a send() call)
2938        let _ = ctx.svm.set_account(
2939            pk_cpi,
2940            Account {
2941                lamports: 200,
2942                data: vec![2],
2943                owner,
2944                executable: false,
2945                rent_epoch: 0,
2946            },
2947        );
2948        ctx.dirty_tracker.mark_account_dirty(&pk_cpi);
2949
2950        // take_snapshot should include pk_cpi even though it's not in tracked_accounts
2951        ctx.take_snapshot();
2952        assert!(ctx.has_snapshot());
2953
2954        ctx.begin_iteration();
2955
2956        // Modify the CPI-created account
2957        let _ = ctx.svm.set_account(
2958            pk_cpi,
2959            Account {
2960                lamports: 999,
2961                data: vec![9],
2962                owner,
2963                executable: false,
2964                rent_epoch: 0,
2965            },
2966        );
2967        ctx.dirty_tracker.mark_account_dirty(&pk_cpi);
2968
2969        // Restore should bring it back
2970        ctx.restore_snapshot();
2971        let acc = ctx.svm.get_account(&pk_cpi).unwrap();
2972        assert_eq!(acc.lamports, 200);
2973        assert_eq!(acc.data, vec![2]);
2974    }
2975
2976    #[test]
2977    fn test_snapshot_programs_arc_clone() {
2978        let mut ctx = TestContext::new();
2979
2980        // Verify programs field uses Arc (clone is cheap)
2981        let pk = Pubkey::new_unique();
2982        ctx.write_account(
2983            &pk,
2984            Account {
2985                lamports: 100,
2986                data: vec![0; 1024],
2987                owner: Pubkey::new_unique(),
2988                executable: false,
2989                rent_epoch: 0,
2990            },
2991        )
2992        .unwrap();
2993
2994        // Clone should share program data via Arc
2995        let cloned = ctx.clone();
2996        assert_eq!(ctx.programs_count(), cloned.programs_count());
2997    }
2998
2999    // =========================================================================
3000    // parse_error_code — TransactionError → Custom(N)
3001    // =========================================================================
3002
3003    #[test]
3004    fn parse_error_code_custom() {
3005        use solana_instruction::error::InstructionError;
3006        let err = TransactionError::InstructionError(0, InstructionError::Custom(6051));
3007        assert_eq!(parse_error_code(&err), Some(6051));
3008    }
3009
3010    #[test]
3011    fn parse_error_code_custom_zero() {
3012        use solana_instruction::error::InstructionError;
3013        let err = TransactionError::InstructionError(1, InstructionError::Custom(0));
3014        assert_eq!(parse_error_code(&err), Some(0));
3015    }
3016
3017    #[test]
3018    fn parse_error_code_custom_large() {
3019        use solana_instruction::error::InstructionError;
3020        let err = TransactionError::InstructionError(0, InstructionError::Custom(u32::MAX));
3021        assert_eq!(parse_error_code(&err), Some(u32::MAX));
3022    }
3023
3024    #[test]
3025    fn parse_error_code_non_custom_instruction_error() {
3026        use solana_instruction::error::InstructionError;
3027        let err = TransactionError::InstructionError(0, InstructionError::GenericError);
3028        assert_eq!(parse_error_code(&err), None);
3029    }
3030
3031    #[test]
3032    fn parse_error_code_non_instruction_error() {
3033        let err = TransactionError::AccountInUse;
3034        assert_eq!(parse_error_code(&err), None);
3035    }
3036
3037    // =========================================================================
3038    // parse_instruction_index — TransactionError → instruction index
3039    // =========================================================================
3040
3041    #[test]
3042    fn parse_instruction_index_basic() {
3043        use solana_instruction::error::InstructionError;
3044        let err = TransactionError::InstructionError(0, InstructionError::Custom(42));
3045        assert_eq!(parse_instruction_index(&err), Some(0));
3046    }
3047
3048    #[test]
3049    fn parse_instruction_index_nonzero() {
3050        use solana_instruction::error::InstructionError;
3051        let err = TransactionError::InstructionError(3, InstructionError::GenericError);
3052        assert_eq!(parse_instruction_index(&err), Some(3));
3053    }
3054
3055    #[test]
3056    fn parse_instruction_index_max_u8() {
3057        use solana_instruction::error::InstructionError;
3058        let err = TransactionError::InstructionError(255, InstructionError::Custom(1));
3059        assert_eq!(parse_instruction_index(&err), Some(255));
3060    }
3061
3062    #[test]
3063    fn parse_instruction_index_non_instruction_error() {
3064        let err = TransactionError::AccountInUse;
3065        assert_eq!(parse_instruction_index(&err), None);
3066    }
3067
3068    // =========================================================================
3069    // format_action_sequence — TLS action history formatting
3070    // =========================================================================
3071
3072    /// Helper: clear all TLS state used by format_action_sequence / format_last_action_oneline
3073    fn clear_format_tls() {
3074        clear_action_history();
3075        clear_violation_tracking();
3076    }
3077
3078    #[test]
3079    fn format_action_sequence_empty() {
3080        clear_format_tls();
3081        let out = format_action_sequence();
3082        assert!(out.is_empty(), "expected empty for no history: {:?}", out);
3083    }
3084
3085    #[test]
3086    fn format_action_sequence_single_ok() {
3087        clear_format_tls();
3088        set_total_actions(1);
3089        push_action_record("action_deposit", serde_json::json!({"amount": 500}), true);
3090
3091        let out = format_action_sequence();
3092        assert!(out.contains("1 executed, 0 skipped"), "header: {out}");
3093        assert!(
3094            out.contains("action_deposit(amount=500) -> OK"),
3095            "body: {out}"
3096        );
3097    }
3098
3099    #[test]
3100    fn format_action_sequence_single_fail() {
3101        clear_format_tls();
3102        set_total_actions(1);
3103        push_action_record("action_withdraw", serde_json::json!({}), false);
3104
3105        let out = format_action_sequence();
3106        assert!(out.contains("action_withdraw -> FAIL"), "body: {out}");
3107    }
3108
3109    #[test]
3110    fn format_action_sequence_multiple_params() {
3111        clear_format_tls();
3112        set_total_actions(3);
3113        push_action_record(
3114            "action_deposit",
3115            serde_json::json!({"user": 0, "amount": 100}),
3116            true,
3117        );
3118        push_action_record("action_borrow", serde_json::json!({"user": 1}), true);
3119        push_action_record("action_repay", serde_json::json!({}), false);
3120
3121        let out = format_action_sequence();
3122        assert!(out.contains("3 executed, 0 skipped"), "header: {out}");
3123        assert!(out.contains("1. action_deposit("), "first: {out}");
3124        assert!(
3125            out.contains("2. action_borrow(user=1) -> OK"),
3126            "second: {out}"
3127        );
3128        assert!(out.contains("3. action_repay -> FAIL"), "third: {out}");
3129    }
3130
3131    #[test]
3132    fn format_action_sequence_with_violation_marker() {
3133        clear_format_tls();
3134        set_total_actions(3);
3135        push_action_record("action_a", serde_json::json!({}), true);
3136        push_action_record("action_b", serde_json::json!({}), true);
3137        // Action at index 1 triggered the violation
3138        set_violation_action_index(1);
3139
3140        let out = format_action_sequence();
3141        assert!(out.contains("2 executed, 1 skipped"), "header: {out}");
3142        assert!(
3143            !out.contains("1. action_a")
3144                || !out.contains("[VIOLATION]")
3145                || out.contains("action_a -> OK\n")
3146                || !out.matches("[VIOLATION]").count() > 1,
3147            "violation should only be on action_b"
3148        );
3149        assert!(
3150            out.contains("action_b -> OK [VIOLATION]"),
3151            "violation marker: {out}"
3152        );
3153        assert!(out.contains("1 action(s) not executed"), "skipped: {out}");
3154    }
3155
3156    #[test]
3157    fn format_action_sequence_skipped_actions() {
3158        clear_format_tls();
3159        set_total_actions(10);
3160        push_action_record("action_only", serde_json::json!({}), true);
3161
3162        let out = format_action_sequence();
3163        assert!(out.contains("1 executed, 9 skipped"), "header: {out}");
3164        assert!(out.contains("9 action(s) not executed"), "footer: {out}");
3165    }
3166
3167    #[test]
3168    fn format_action_sequence_no_params_no_parens() {
3169        clear_format_tls();
3170        set_total_actions(1);
3171        // Null params (from push_action_record_lite)
3172        ACTION_HISTORY.with(|h| {
3173            h.borrow_mut().push(ActionRecord {
3174                name: "action_foo".to_string(),
3175                params: serde_json::Value::Null,
3176                success: true,
3177                error_code: None,
3178            });
3179        });
3180
3181        let out = format_action_sequence();
3182        // Should NOT have parentheses for null params
3183        assert!(out.contains("action_foo -> OK"), "body: {out}");
3184        assert!(
3185            !out.contains("action_foo("),
3186            "should not have parens: {out}"
3187        );
3188    }
3189
3190    // =========================================================================
3191    // format_last_action_oneline — last action summary
3192    // =========================================================================
3193
3194    #[test]
3195    fn format_last_action_oneline_empty() {
3196        clear_format_tls();
3197        let out = format_last_action_oneline();
3198        assert!(out.is_empty());
3199    }
3200
3201    #[test]
3202    fn format_last_action_oneline_success_with_params() {
3203        clear_format_tls();
3204        push_action_record("action_deposit", serde_json::json!({"amount": 42}), true);
3205
3206        let out = format_last_action_oneline();
3207        assert_eq!(out, "action_deposit(amount=42) -> OK");
3208    }
3209
3210    #[test]
3211    fn format_last_action_oneline_fail_no_params() {
3212        clear_format_tls();
3213        push_action_record("action_withdraw", serde_json::json!({}), false);
3214
3215        let out = format_last_action_oneline();
3216        assert_eq!(out, "action_withdraw -> FAIL");
3217    }
3218
3219    #[test]
3220    fn format_last_action_oneline_returns_last() {
3221        clear_format_tls();
3222        push_action_record("action_first", serde_json::json!({}), true);
3223        push_action_record("action_second", serde_json::json!({"x": 1}), false);
3224
3225        let out = format_last_action_oneline();
3226        assert!(
3227            out.starts_with("action_second"),
3228            "should return last: {out}"
3229        );
3230        assert!(out.contains("FAIL"), "last was failure: {out}");
3231    }
3232
3233    #[test]
3234    fn format_last_action_oneline_null_params() {
3235        clear_format_tls();
3236        ACTION_HISTORY.with(|h| {
3237            h.borrow_mut().push(ActionRecord {
3238                name: "action_lite".to_string(),
3239                params: serde_json::Value::Null,
3240                success: true,
3241                error_code: None,
3242            });
3243        });
3244
3245        let out = format_last_action_oneline();
3246        assert_eq!(out, "action_lite -> OK");
3247    }
3248
3249    // =========================================================================
3250    // format_json_value — compact JSON formatting (tested via format_action_sequence)
3251    // =========================================================================
3252
3253    #[test]
3254    fn format_action_sequence_nested_params() {
3255        clear_format_tls();
3256        set_total_actions(1);
3257        push_action_record(
3258            "action_complex",
3259            serde_json::json!({"arr": [1, 2], "flag": true, "label": "hi"}),
3260            true,
3261        );
3262
3263        let out = format_action_sequence();
3264        assert!(out.contains("arr=[1, 2]"), "array param: {out}");
3265        assert!(out.contains("flag=true"), "bool param: {out}");
3266        assert!(out.contains("label=\"hi\""), "string param: {out}");
3267    }
3268
3269    // =========================================================================
3270    // write_crash_metadata — file I/O
3271    // =========================================================================
3272
3273    #[test]
3274    fn write_crash_metadata_creates_files() {
3275        let tmp = tempfile::tempdir().unwrap();
3276        let crash_dir = tmp.path().to_str().unwrap();
3277
3278        // Set up TLS state
3279        clear_format_tls();
3280        set_current_test_name("my_test");
3281        set_current_iteration(42);
3282        push_action_record("action_a", serde_json::json!({"x": 1}), true);
3283
3284        let input_bytes = b"crash_input_data";
3285        let hash: u64 = 0xDEADBEEF;
3286        write_crash_metadata(crash_dir, hash, Some(999), input_bytes);
3287
3288        let crash_id = format!("crash_{:016x}", hash);
3289
3290        // Check input file exists and matches
3291        let input_path = tmp.path().join(&crash_id);
3292        assert!(input_path.exists(), "input file should exist");
3293        assert_eq!(std::fs::read(&input_path).unwrap(), input_bytes);
3294
3295        // Check metadata file exists and is valid JSON
3296        let meta_path = tmp.path().join(format!("{}.meta.json", crash_id));
3297        assert!(meta_path.exists(), "meta file should exist");
3298        let meta_str = std::fs::read_to_string(&meta_path).unwrap();
3299        let meta: serde_json::Value = serde_json::from_str(&meta_str).unwrap();
3300
3301        assert_eq!(meta["test_name"], "my_test");
3302        assert_eq!(meta["iteration"], 42);
3303        assert_eq!(meta["seed"], 999);
3304        assert_eq!(meta["actions"].as_array().unwrap().len(), 1);
3305        assert_eq!(meta["actions"][0]["name"], "action_a");
3306    }
3307
3308    #[test]
3309    fn write_crash_metadata_no_seed() {
3310        let tmp = tempfile::tempdir().unwrap();
3311        let crash_dir = tmp.path().to_str().unwrap();
3312
3313        clear_format_tls();
3314        set_current_test_name("test2");
3315
3316        write_crash_metadata(crash_dir, 0x1234, None, b"data");
3317
3318        let meta_path = tmp.path().join("crash_0000000000001234.meta.json");
3319        let meta_str = std::fs::read_to_string(&meta_path).unwrap();
3320        let meta: serde_json::Value = serde_json::from_str(&meta_str).unwrap();
3321
3322        // seed should be absent (skip_serializing_if = None)
3323        assert!(
3324            meta.get("seed").is_none() || meta["seed"].is_null(),
3325            "seed should be absent or null: {:?}",
3326            meta.get("seed")
3327        );
3328    }
3329
3330    // =========================================================================
3331    // write_crash_metadata_for_id — update metadata in place
3332    // =========================================================================
3333
3334    #[test]
3335    fn write_crash_metadata_for_id_creates_meta() {
3336        let tmp = tempfile::tempdir().unwrap();
3337        let crash_dir = tmp.path().to_str().unwrap();
3338
3339        clear_format_tls();
3340        set_current_test_name("tmin_test");
3341        set_current_iteration(7);
3342        push_action_record("action_min", serde_json::json!({}), false);
3343
3344        write_crash_metadata_for_id(crash_dir, "crash_abc", Some(55));
3345
3346        let meta_path = tmp.path().join("crash_abc.meta.json");
3347        assert!(meta_path.exists(), "meta file should exist");
3348        let meta: serde_json::Value =
3349            serde_json::from_str(&std::fs::read_to_string(&meta_path).unwrap()).unwrap();
3350
3351        assert_eq!(meta["test_name"], "tmin_test");
3352        assert_eq!(meta["seed"], 55);
3353        assert_eq!(meta["actions"][0]["name"], "action_min");
3354        assert_eq!(meta["actions"][0]["success"], false);
3355    }
3356
3357    // =========================================================================
3358    // IntoActionSuccess trait
3359    // =========================================================================
3360
3361    #[test]
3362    fn into_action_success_unit() {
3363        assert!(().into_success());
3364    }
3365
3366    #[test]
3367    fn into_action_success_result_ok() {
3368        let r: Result<(), String> = Ok(());
3369        assert!(r.into_success());
3370    }
3371
3372    #[test]
3373    fn into_action_success_result_err() {
3374        let r: Result<(), &str> = Err("boom");
3375        assert!(!r.into_success());
3376    }
3377
3378    #[test]
3379    fn into_action_success_bool() {
3380        assert!(true.into_success());
3381        assert!(!false.into_success());
3382    }
3383
3384    // =========================================================================
3385    // Action history helpers
3386    // =========================================================================
3387
3388    #[test]
3389    fn action_history_push_and_clear() {
3390        clear_action_history();
3391        push_action_record("a1", serde_json::json!({}), true);
3392        push_action_record("a2", serde_json::json!({"k": "v"}), false);
3393        let h = get_action_history();
3394        assert_eq!(h.len(), 2);
3395        assert_eq!(h[0].name, "a1");
3396        assert!(h[0].success);
3397        assert_eq!(h[1].name, "a2");
3398        assert!(!h[1].success);
3399
3400        clear_action_history();
3401        assert!(get_action_history().is_empty());
3402    }
3403
3404    #[test]
3405    fn backfill_action_params_updates_entry() {
3406        clear_action_history();
3407        push_action_record_lite("action_x", true);
3408        let h = get_action_history();
3409        assert!(h[0].params.is_null(), "lite record should have null params");
3410
3411        backfill_action_params(0, serde_json::json!({"filled": true}));
3412        let h = get_action_history();
3413        assert_eq!(h[0].params["filled"], true);
3414    }
3415
3416    #[test]
3417    fn backfill_action_params_out_of_bounds_noop() {
3418        clear_action_history();
3419        push_action_record_lite("a", true);
3420        // Should not panic on out-of-bounds index
3421        backfill_action_params(99, serde_json::json!({"x": 1}));
3422        let h = get_action_history();
3423        assert!(h[0].params.is_null(), "original should be unchanged");
3424    }
3425
3426    // =========================================================================
3427    // Violation tracking (index + clearing)
3428    // =========================================================================
3429
3430    #[test]
3431    fn violation_action_index_only_records_first() {
3432        clear_violation_tracking();
3433        set_violation_action_index(3);
3434        set_violation_action_index(7); // should be ignored
3435        assert_eq!(get_violation_action_index(), Some(3));
3436    }
3437
3438    #[test]
3439    fn clear_violation_tracking_resets_all() {
3440        set_total_actions(10);
3441        set_violation_action_index(5);
3442        clear_violation_tracking();
3443
3444        assert_eq!(get_violation_action_index(), None);
3445        // total_actions is internal, but format_action_sequence will show 0 skipped
3446        clear_action_history();
3447        let out = format_action_sequence();
3448        assert!(out.is_empty(), "should be empty after full clear");
3449    }
3450
3451    // =========================================================================
3452    // Success-seeking helpers
3453    // =========================================================================
3454
3455    #[test]
3456    fn succeeded_variants_tracking() {
3457        // Clear state
3458        SUCCEEDED_VARIANTS.with(|s| s.borrow_mut().clear());
3459
3460        assert!(!has_variant_succeeded(0));
3461        assert!(!has_variant_succeeded(1));
3462        assert_eq!(succeeded_variant_count(), 0);
3463
3464        mark_variant_succeeded(0);
3465        assert!(has_variant_succeeded(0));
3466        assert!(!has_variant_succeeded(1));
3467        assert_eq!(succeeded_variant_count(), 1);
3468
3469        mark_variant_succeeded(0); // duplicate
3470        assert_eq!(succeeded_variant_count(), 1);
3471
3472        mark_variant_succeeded(5);
3473        assert_eq!(succeeded_variant_count(), 2);
3474    }
3475
3476    // =========================================================================
3477    // Dispatch counting
3478    // =========================================================================
3479
3480    #[test]
3481    fn dispatch_count_reset_and_increment() {
3482        reset_iteration_dispatch_count();
3483        // min is 1 even when 0 dispatches
3484        assert_eq!(get_iteration_dispatch_count(), 1);
3485
3486        increment_action_count();
3487        increment_action_count();
3488        assert_eq!(get_iteration_dispatch_count(), 2);
3489
3490        reset_iteration_dispatch_count();
3491        assert_eq!(get_iteration_dispatch_count(), 1);
3492    }
3493
3494    // =========================================================================
3495    // build_crash_metadata
3496    // =========================================================================
3497
3498    #[test]
3499    fn build_crash_metadata_captures_tls() {
3500        clear_format_tls();
3501        set_current_test_name("crash_test");
3502        set_current_iteration(99);
3503        push_action_record("act_a", serde_json::json!({"p": 1}), true);
3504        push_action_record("act_b", serde_json::json!({}), false);
3505
3506        let meta = build_crash_metadata(Some(12345));
3507        assert_eq!(meta.test_name, "crash_test");
3508        assert_eq!(meta.iteration, 99);
3509        assert_eq!(meta.seed, Some(12345));
3510        assert_eq!(meta.actions.len(), 2);
3511        assert_eq!(meta.actions[0].name, "act_a");
3512        assert!(meta.actions[0].success);
3513        assert_eq!(meta.actions[1].name, "act_b");
3514        assert!(!meta.actions[1].success);
3515    }
3516
3517    #[test]
3518    fn build_crash_metadata_no_test_name() {
3519        clear_format_tls();
3520        CURRENT_TEST_NAME.with(|t| *t.borrow_mut() = None);
3521
3522        let meta = build_crash_metadata(None);
3523        assert_eq!(meta.test_name, "unknown");
3524        assert!(meta.seed.is_none());
3525    }
3526
3527    // =========================================================================
3528    // GenericAccountBuilder
3529    // =========================================================================
3530
3531    #[test]
3532    fn generic_builder_create_basic() {
3533        let mut ctx = TestContext::new();
3534        let pk = Pubkey::new_unique();
3535        let owner = Pubkey::new_unique();
3536
3537        let addr = ctx
3538            .create_account()
3539            .pubkey(pk)
3540            .owner(owner)
3541            .lamports(1_000_000)
3542            .size(128)
3543            .create()
3544            .unwrap();
3545
3546        assert_eq!(addr, pk);
3547        let acc = ctx.svm.get_account(&pk).unwrap();
3548        assert_eq!(acc.owner, owner);
3549        assert_eq!(acc.lamports, 1_000_000);
3550        assert_eq!(acc.data.len(), 128);
3551        assert!(!acc.executable);
3552    }
3553
3554    #[test]
3555    fn generic_builder_with_data() {
3556        let mut ctx = TestContext::new();
3557        let pk = Pubkey::new_unique();
3558        let data = vec![1, 2, 3, 4, 5];
3559
3560        ctx.create_account()
3561            .pubkey(pk)
3562            .lamports(1)
3563            .data(&data)
3564            .create()
3565            .unwrap();
3566
3567        let acc = ctx.svm.get_account(&pk).unwrap();
3568        assert_eq!(acc.data, data);
3569    }
3570
3571    #[test]
3572    fn generic_builder_default_address_errors() {
3573        let mut ctx = TestContext::new();
3574        let err = ctx.create_account().lamports(100).create().unwrap_err();
3575
3576        assert!(
3577            err.to_string().contains("Address must be set"),
3578            "expected address error: {}",
3579            err
3580        );
3581    }
3582
3583    #[test]
3584    fn generic_builder_tracks_account() {
3585        let mut ctx = TestContext::new();
3586        let pk = Pubkey::new_unique();
3587        let before = ctx.tracked_accounts_count();
3588
3589        ctx.create_account().pubkey(pk).create().unwrap();
3590
3591        assert_eq!(ctx.tracked_accounts_count(), before + 1);
3592    }
3593
3594    // =========================================================================
3595    // MintAccountBuilder
3596    // =========================================================================
3597
3598    #[test]
3599    fn mint_builder_create_basic() {
3600        let mut ctx = TestContext::new();
3601        let mint_pk = Pubkey::new_unique();
3602        let authority = Pubkey::new_unique();
3603
3604        let addr = ctx
3605            .create_mint()
3606            .pubkey(mint_pk)
3607            .mint_authority(authority)
3608            .decimals(6)
3609            .supply(1_000_000)
3610            .create()
3611            .unwrap();
3612
3613        assert_eq!(addr, mint_pk);
3614
3615        let acc = ctx.svm.get_account(&mint_pk).unwrap();
3616        assert_eq!(acc.owner, spl_token::id());
3617        assert_eq!(acc.data.len(), spl_token::state::Mint::LEN);
3618
3619        // Deserialize and verify packed state
3620        let mint = spl_token::state::Mint::unpack(&acc.data).unwrap();
3621        assert_eq!(mint.decimals, 6);
3622        assert_eq!(mint.supply, 1_000_000);
3623        assert_eq!(mint.mint_authority, COption::Some(authority));
3624        assert!(mint.is_initialized);
3625    }
3626
3627    #[test]
3628    fn mint_builder_default_is_initialized() {
3629        let mut ctx = TestContext::new();
3630        let pk = Pubkey::new_unique();
3631
3632        ctx.create_mint().pubkey(pk).create().unwrap();
3633
3634        let acc = ctx.svm.get_account(&pk).unwrap();
3635        let mint = spl_token::state::Mint::unpack(&acc.data).unwrap();
3636        assert!(mint.is_initialized, "default mint should be initialized");
3637    }
3638
3639    #[test]
3640    fn mint_builder_freeze_authority() {
3641        let mut ctx = TestContext::new();
3642        let pk = Pubkey::new_unique();
3643        let freeze_auth = Pubkey::new_unique();
3644
3645        ctx.create_mint()
3646            .pubkey(pk)
3647            .freeze_authority(Some(freeze_auth))
3648            .create()
3649            .unwrap();
3650
3651        let acc = ctx.svm.get_account(&pk).unwrap();
3652        let mint = spl_token::state::Mint::unpack(&acc.data).unwrap();
3653        assert_eq!(mint.freeze_authority, COption::Some(freeze_auth));
3654    }
3655
3656    #[test]
3657    fn mint_builder_freeze_authority_none() {
3658        let mut ctx = TestContext::new();
3659        let pk = Pubkey::new_unique();
3660
3661        ctx.create_mint()
3662            .pubkey(pk)
3663            .freeze_authority(None)
3664            .create()
3665            .unwrap();
3666
3667        let acc = ctx.svm.get_account(&pk).unwrap();
3668        let mint = spl_token::state::Mint::unpack(&acc.data).unwrap();
3669        assert_eq!(mint.freeze_authority, COption::None);
3670    }
3671
3672    #[test]
3673    fn mint_builder_default_address_errors() {
3674        let mut ctx = TestContext::new();
3675        let err = ctx.create_mint().decimals(9).create().unwrap_err();
3676
3677        assert!(
3678            err.to_string().contains("Address must be set"),
3679            "expected address error: {}",
3680            err
3681        );
3682    }
3683
3684    #[test]
3685    fn mint_builder_has_rent_exempt_lamports() {
3686        let mut ctx = TestContext::new();
3687        let pk = Pubkey::new_unique();
3688
3689        ctx.create_mint().pubkey(pk).create().unwrap();
3690
3691        let acc = ctx.svm.get_account(&pk).unwrap();
3692        let rent = Rent::default();
3693        let min_lamports = rent.minimum_balance(spl_token::state::Mint::LEN);
3694        assert_eq!(
3695            acc.lamports, min_lamports,
3696            "mint should have rent-exempt lamports by default"
3697        );
3698    }
3699
3700    // =========================================================================
3701    // TokenAccountBuilder
3702    // =========================================================================
3703
3704    #[test]
3705    fn token_builder_create_basic() {
3706        let mut ctx = TestContext::new();
3707        let token_pk = Pubkey::new_unique();
3708        let mint_pk = Pubkey::new_unique();
3709        let owner_pk = Pubkey::new_unique();
3710
3711        let addr = ctx
3712            .create_token_account()
3713            .pubkey(token_pk)
3714            .mint(mint_pk)
3715            .token_owner(owner_pk)
3716            .amount(500)
3717            .create()
3718            .unwrap();
3719
3720        assert_eq!(addr, token_pk);
3721
3722        let acc = ctx.svm.get_account(&token_pk).unwrap();
3723        assert_eq!(acc.owner, spl_token::id());
3724        assert_eq!(acc.data.len(), spl_token::state::Account::LEN);
3725
3726        let token = spl_token::state::Account::unpack(&acc.data).unwrap();
3727        assert_eq!(token.mint, mint_pk);
3728        assert_eq!(token.owner, owner_pk);
3729        assert_eq!(token.amount, 500);
3730        assert_eq!(token.state, spl_token::state::AccountState::Initialized);
3731    }
3732
3733    #[test]
3734    fn token_builder_missing_mint_errors() {
3735        let mut ctx = TestContext::new();
3736        let pk = Pubkey::new_unique();
3737        let owner = Pubkey::new_unique();
3738
3739        let err = ctx
3740            .create_token_account()
3741            .pubkey(pk)
3742            .token_owner(owner)
3743            .create()
3744            .unwrap_err();
3745
3746        assert!(
3747            err.to_string().contains("Mint must be set"),
3748            "expected mint error: {}",
3749            err
3750        );
3751    }
3752
3753    #[test]
3754    fn token_builder_missing_owner_errors() {
3755        let mut ctx = TestContext::new();
3756        let pk = Pubkey::new_unique();
3757        let mint = Pubkey::new_unique();
3758
3759        let err = ctx
3760            .create_token_account()
3761            .pubkey(pk)
3762            .mint(mint)
3763            .create()
3764            .unwrap_err();
3765
3766        assert!(
3767            err.to_string().contains("Owner must be set"),
3768            "expected owner error: {}",
3769            err
3770        );
3771    }
3772
3773    #[test]
3774    fn token_builder_default_address_errors() {
3775        let mut ctx = TestContext::new();
3776        let mint = Pubkey::new_unique();
3777        let owner = Pubkey::new_unique();
3778
3779        let err = ctx
3780            .create_token_account()
3781            .mint(mint)
3782            .token_owner(owner)
3783            .create()
3784            .unwrap_err();
3785
3786        assert!(
3787            err.to_string().contains("Address must be set"),
3788            "expected address error: {}",
3789            err
3790        );
3791    }
3792
3793    #[test]
3794    fn token_builder_delegate() {
3795        let mut ctx = TestContext::new();
3796        let pk = Pubkey::new_unique();
3797        let mint = Pubkey::new_unique();
3798        let owner = Pubkey::new_unique();
3799        let delegate = Pubkey::new_unique();
3800
3801        ctx.create_token_account()
3802            .pubkey(pk)
3803            .mint(mint)
3804            .token_owner(owner)
3805            .delegate(Some(delegate))
3806            .delegated_amount(100)
3807            .create()
3808            .unwrap();
3809
3810        let acc = ctx.svm.get_account(&pk).unwrap();
3811        let token = spl_token::state::Account::unpack(&acc.data).unwrap();
3812        assert_eq!(token.delegate, COption::Some(delegate));
3813        assert_eq!(token.delegated_amount, 100);
3814    }
3815
3816    #[test]
3817    fn token_builder_close_authority() {
3818        let mut ctx = TestContext::new();
3819        let pk = Pubkey::new_unique();
3820        let mint = Pubkey::new_unique();
3821        let owner = Pubkey::new_unique();
3822        let close_auth = Pubkey::new_unique();
3823
3824        ctx.create_token_account()
3825            .pubkey(pk)
3826            .mint(mint)
3827            .token_owner(owner)
3828            .close_authority(Some(close_auth))
3829            .create()
3830            .unwrap();
3831
3832        let acc = ctx.svm.get_account(&pk).unwrap();
3833        let token = spl_token::state::Account::unpack(&acc.data).unwrap();
3834        assert_eq!(token.close_authority, COption::Some(close_auth));
3835    }
3836
3837    #[test]
3838    fn token_builder_is_native() {
3839        let mut ctx = TestContext::new();
3840        let pk = Pubkey::new_unique();
3841        let mint = Pubkey::new_unique();
3842        let owner = Pubkey::new_unique();
3843
3844        ctx.create_token_account()
3845            .pubkey(pk)
3846            .mint(mint)
3847            .token_owner(owner)
3848            .is_native(Some(1_000_000))
3849            .create()
3850            .unwrap();
3851
3852        let acc = ctx.svm.get_account(&pk).unwrap();
3853        let token = spl_token::state::Account::unpack(&acc.data).unwrap();
3854        assert_eq!(token.is_native, COption::Some(1_000_000));
3855    }
3856
3857    #[test]
3858    fn token_builder_has_rent_exempt_lamports() {
3859        let mut ctx = TestContext::new();
3860        let pk = Pubkey::new_unique();
3861        let mint = Pubkey::new_unique();
3862        let owner = Pubkey::new_unique();
3863
3864        ctx.create_token_account()
3865            .pubkey(pk)
3866            .mint(mint)
3867            .token_owner(owner)
3868            .create()
3869            .unwrap();
3870
3871        let acc = ctx.svm.get_account(&pk).unwrap();
3872        let rent = Rent::default();
3873        let min_lamports = rent.minimum_balance(spl_token::state::Account::LEN);
3874        assert_eq!(
3875            acc.lamports, min_lamports,
3876            "token account should have rent-exempt lamports by default"
3877        );
3878    }
3879
3880    // =========================================================================
3881    // AccountBuilderBase trait — shared builder methods
3882    // =========================================================================
3883
3884    #[test]
3885    fn builder_rent_epoch() {
3886        let mut ctx = TestContext::new();
3887        let pk = Pubkey::new_unique();
3888
3889        ctx.create_account()
3890            .pubkey(pk)
3891            .lamports(1)
3892            .rent_epoch(42)
3893            .create()
3894            .unwrap();
3895
3896        let acc = ctx.svm.get_account(&pk).unwrap();
3897        assert_eq!(acc.rent_epoch, 42);
3898    }
3899
3900    #[test]
3901    fn builder_lamports_override_on_mint() {
3902        let mut ctx = TestContext::new();
3903        let pk = Pubkey::new_unique();
3904
3905        // Override default rent-exempt lamports
3906        ctx.create_mint().pubkey(pk).lamports(999).create().unwrap();
3907
3908        let acc = ctx.svm.get_account(&pk).unwrap();
3909        assert_eq!(acc.lamports, 999);
3910    }
3911
3912    // =========================================================================
3913    // Account builder — deeper edge cases
3914    // =========================================================================
3915
3916    #[test]
3917    fn generic_builder_overwrite_same_pubkey() {
3918        let mut ctx = TestContext::new();
3919        let pk = Pubkey::new_unique();
3920
3921        // First create
3922        ctx.create_account()
3923            .pubkey(pk)
3924            .lamports(100)
3925            .data(&[1, 2, 3])
3926            .create()
3927            .unwrap();
3928        let acc1 = ctx.svm.get_account(&pk).unwrap();
3929        assert_eq!(acc1.data, vec![1, 2, 3]);
3930
3931        // Second create overwrites
3932        ctx.create_account()
3933            .pubkey(pk)
3934            .lamports(200)
3935            .data(&[4, 5])
3936            .create()
3937            .unwrap();
3938        let acc2 = ctx.svm.get_account(&pk).unwrap();
3939        assert_eq!(acc2.data, vec![4, 5]);
3940        assert_eq!(acc2.lamports, 200);
3941    }
3942
3943    #[test]
3944    fn mint_builder_zero_decimals() {
3945        // NFT use case: 0 decimals is valid
3946        let mut ctx = TestContext::new();
3947        let pk = Pubkey::new_unique();
3948
3949        ctx.create_mint().pubkey(pk).decimals(0).create().unwrap();
3950
3951        let acc = ctx.svm.get_account(&pk).unwrap();
3952        let mint = spl_token::state::Mint::unpack(&acc.data).unwrap();
3953        assert_eq!(mint.decimals, 0);
3954    }
3955
3956    #[test]
3957    fn token_builder_max_amount() {
3958        let mut ctx = TestContext::new();
3959        let pk = Pubkey::new_unique();
3960        let mint = Pubkey::new_unique();
3961        let owner = Pubkey::new_unique();
3962
3963        ctx.create_token_account()
3964            .pubkey(pk)
3965            .mint(mint)
3966            .token_owner(owner)
3967            .amount(u64::MAX)
3968            .create()
3969            .unwrap();
3970
3971        let acc = ctx.svm.get_account(&pk).unwrap();
3972        let token = spl_token::state::Account::unpack(&acc.data).unwrap();
3973        assert_eq!(token.amount, u64::MAX);
3974    }
3975
3976    #[test]
3977    fn token_builder_frozen_state() {
3978        let mut ctx = TestContext::new();
3979        let pk = Pubkey::new_unique();
3980        let mint = Pubkey::new_unique();
3981        let owner = Pubkey::new_unique();
3982
3983        ctx.create_token_account()
3984            .pubkey(pk)
3985            .mint(mint)
3986            .token_owner(owner)
3987            .state(spl_token::state::AccountState::Frozen)
3988            .create()
3989            .unwrap();
3990
3991        let acc = ctx.svm.get_account(&pk).unwrap();
3992        let token = spl_token::state::Account::unpack(&acc.data).unwrap();
3993        assert_eq!(token.state, spl_token::state::AccountState::Frozen);
3994    }
3995
3996    #[test]
3997    fn builder_chaining_order_independent() {
3998        let mut ctx = TestContext::new();
3999        let mint = Pubkey::new_unique();
4000        let owner = Pubkey::new_unique();
4001        let pk1 = Pubkey::new_unique();
4002        let pk2 = Pubkey::new_unique();
4003
4004        // Order 1: mint → pubkey → token_owner → amount
4005        ctx.create_token_account()
4006            .mint(mint)
4007            .pubkey(pk1)
4008            .token_owner(owner)
4009            .amount(42)
4010            .create()
4011            .unwrap();
4012
4013        // Order 2: amount → token_owner → pubkey → mint
4014        ctx.create_token_account()
4015            .amount(42)
4016            .token_owner(owner)
4017            .pubkey(pk2)
4018            .mint(mint)
4019            .create()
4020            .unwrap();
4021
4022        let acc1 = ctx.svm.get_account(&pk1).unwrap();
4023        let acc2 = ctx.svm.get_account(&pk2).unwrap();
4024        let token1 = spl_token::state::Account::unpack(&acc1.data).unwrap();
4025        let token2 = spl_token::state::Account::unpack(&acc2.data).unwrap();
4026
4027        assert_eq!(token1.mint, token2.mint);
4028        assert_eq!(token1.owner, token2.owner);
4029        assert_eq!(token1.amount, token2.amount);
4030    }
4031
4032    #[test]
4033    fn generic_builder_zero_length_data_with_lamports() {
4034        let mut ctx = TestContext::new();
4035        let pk = Pubkey::new_unique();
4036
4037        ctx.create_account()
4038            .pubkey(pk)
4039            .lamports(1_000_000)
4040            .size(0) // zero-length data
4041            .create()
4042            .unwrap();
4043
4044        let acc = ctx.svm.get_account(&pk).unwrap();
4045        assert_eq!(acc.lamports, 1_000_000);
4046        assert!(acc.data.is_empty());
4047    }
4048
4049    // =========================================================================
4050    // format_action_sequence — deeper edge cases
4051    // =========================================================================
4052
4053    #[test]
4054    fn format_action_sequence_violation_at_index_0() {
4055        clear_format_tls();
4056        set_total_actions(2);
4057        push_action_record("action_first", serde_json::json!({}), false);
4058        push_action_record("action_second", serde_json::json!({}), true);
4059        set_violation_action_index(0);
4060
4061        let out = format_action_sequence();
4062        assert!(
4063            out.contains("action_first -> FAIL [VIOLATION]"),
4064            "violation should be on first: {out}"
4065        );
4066        assert!(
4067            !out.contains("action_second -> OK [VIOLATION]"),
4068            "second should not have marker: {out}"
4069        );
4070    }
4071
4072    #[test]
4073    fn format_action_sequence_deeply_nested_params() {
4074        clear_format_tls();
4075        set_total_actions(1);
4076        push_action_record(
4077            "action_nested",
4078            serde_json::json!({"a": {"b": {"c": 1}}}),
4079            true,
4080        );
4081
4082        let out = format_action_sequence();
4083        // format_json_value should recurse into nested objects
4084        assert!(out.contains("a={"), "nested object: {out}");
4085        assert!(out.contains("c: 1"), "deeply nested value: {out}");
4086    }
4087
4088    #[test]
4089    fn format_action_sequence_total_0_with_actions() {
4090        // Inconsistent state: total_actions = 0 but history has entries
4091        clear_format_tls();
4092        // Don't call set_total_actions — defaults to 0
4093        push_action_record("orphan", serde_json::json!({}), true);
4094
4095        let out = format_action_sequence();
4096        // Should still format (total_actions=0 but history non-empty → skipped = 0 - 1 = saturating_sub → 0)
4097        assert!(
4098            out.contains("1 executed, 0 skipped"),
4099            "should handle mismatch: {out}"
4100        );
4101    }
4102
4103    #[test]
4104    fn format_action_sequence_many_actions() {
4105        clear_format_tls();
4106        set_total_actions(50);
4107        for i in 0..50 {
4108            push_action_record(
4109                &format!("action_{}", i),
4110                serde_json::json!({"i": i}),
4111                i % 3 != 0,
4112            );
4113        }
4114
4115        let out = format_action_sequence();
4116        assert!(out.contains("50 executed, 0 skipped"), "header: {out}");
4117        assert!(out.contains("1. action_0"), "first: {out}");
4118        assert!(out.contains("50. action_49"), "last: {out}");
4119    }
4120
4121    #[test]
4122    fn format_last_action_oneline_multiple_params_ordering() {
4123        clear_format_tls();
4124        // serde_json::json! with ordered keys
4125        push_action_record(
4126            "action_multi",
4127            serde_json::json!({"amount": 100, "user": 2}),
4128            true,
4129        );
4130        let out = format_last_action_oneline();
4131        // Should have both params
4132        assert!(out.contains("amount=100"), "amount: {out}");
4133        assert!(out.contains("user=2"), "user: {out}");
4134        assert!(out.contains("-> OK"), "status: {out}");
4135    }
4136
4137    // =========================================================================
4138    // Regression: push_action_record preserves params in output
4139    // (was broken when push_action_record_lite was used instead, dropping params)
4140    // =========================================================================
4141
4142    #[test]
4143    fn action_record_includes_params_in_sequence() {
4144        clear_format_tls();
4145        set_total_actions(3);
4146        push_action_record(
4147            "delegate_stake",
4148            serde_json::json!({"authority": null, "stake_account": 1340788527u64, "vote_account": 640494879u64}),
4149            true,
4150        );
4151        push_action_record("advance_slots", serde_json::json!({"slots": 54177}), true);
4152        push_action_record(
4153            "withdraw",
4154            serde_json::json!({"stake_account": 3, "lamports": 1000, "leave_reserve": true}),
4155            false,
4156        );
4157
4158        let out = format_action_sequence();
4159        // All params must appear in the output
4160        assert!(
4161            out.contains("authority=null"),
4162            "authority param missing: {out}"
4163        );
4164        assert!(
4165            out.contains("stake_account=1340788527"),
4166            "stake_account param missing: {out}"
4167        );
4168        assert!(
4169            out.contains("vote_account=640494879"),
4170            "vote_account param missing: {out}"
4171        );
4172        assert!(out.contains("slots=54177"), "slots param missing: {out}");
4173        assert!(
4174            out.contains("leave_reserve=true"),
4175            "leave_reserve param missing: {out}"
4176        );
4177        assert!(
4178            out.contains("lamports=1000"),
4179            "lamports param missing: {out}"
4180        );
4181        // Status markers
4182        assert!(
4183            out.contains("delegate_stake(") && out.contains(") -> OK"),
4184            "delegate_stake format: {out}"
4185        );
4186        assert!(
4187            out.contains("withdraw(") && out.contains(") -> FAIL"),
4188            "withdraw format: {out}"
4189        );
4190    }
4191
4192    #[test]
4193    fn action_record_params_in_oneline() {
4194        clear_format_tls();
4195        push_action_record(
4196            "move_lamports",
4197            serde_json::json!({"dest_account": 42, "lamports": null, "stake_account": 99}),
4198            true,
4199        );
4200
4201        let out = format_last_action_oneline();
4202        assert!(out.contains("move_lamports("), "should have parens: {out}");
4203        assert!(out.contains("dest_account=42"), "dest_account: {out}");
4204        assert!(out.contains("lamports=null"), "lamports null: {out}");
4205        assert!(out.contains("stake_account=99"), "stake_account: {out}");
4206        assert!(out.contains("-> OK"), "status: {out}");
4207    }
4208
4209    #[test]
4210    fn action_record_lite_omits_params_regression() {
4211        // Verify that push_action_record_lite (the old path) does NOT include params.
4212        // This test documents the regression that was fixed.
4213        clear_format_tls();
4214        set_total_actions(1);
4215        push_action_record_lite("delegate_stake", true);
4216
4217        let out = format_action_sequence();
4218        // Lite records have no params — should NOT have parentheses
4219        assert!(
4220            out.contains("delegate_stake -> OK"),
4221            "should have no params: {out}"
4222        );
4223        assert!(
4224            !out.contains("delegate_stake("),
4225            "should not have parens: {out}"
4226        );
4227    }
4228
4229    // =========================================================================
4230    // parse_error_code — additional variant coverage
4231    // =========================================================================
4232
4233    #[test]
4234    fn parse_error_code_anchor_custom_6000() {
4235        use solana_instruction::error::InstructionError;
4236        let err = TransactionError::InstructionError(0, InstructionError::Custom(6000));
4237        assert_eq!(parse_error_code(&err), Some(6000));
4238    }
4239
4240    #[test]
4241    fn parse_error_code_insufficient_funds() {
4242        use solana_instruction::error::InstructionError;
4243        let err = TransactionError::InstructionError(0, InstructionError::InsufficientFunds);
4244        assert_eq!(
4245            parse_error_code(&err),
4246            None,
4247            "InsufficientFunds has no Custom(N)"
4248        );
4249    }
4250
4251    #[test]
4252    fn parse_error_code_account_already_initialized() {
4253        use solana_instruction::error::InstructionError;
4254        let err =
4255            TransactionError::InstructionError(2, InstructionError::AccountAlreadyInitialized);
4256        assert_eq!(parse_error_code(&err), None);
4257        // But instruction index should still parse
4258        assert_eq!(parse_instruction_index(&err), Some(2));
4259    }
4260
4261    // =========================================================================
4262    // parse_error_code + parse_instruction_index — structural matching
4263    // =========================================================================
4264
4265    #[test]
4266    fn parse_error_code_duplicate_instruction() {
4267        // DuplicateInstruction is NOT InstructionError — should return None
4268        let err = TransactionError::DuplicateInstruction(2);
4269        assert_eq!(parse_error_code(&err), None);
4270        assert_eq!(parse_instruction_index(&err), None);
4271    }
4272
4273    #[test]
4274    fn parse_instruction_index_with_non_custom_error() {
4275        use solana_instruction::error::InstructionError;
4276        // InstructionError that's not Custom — index should still be extracted
4277        let err =
4278            TransactionError::InstructionError(4, InstructionError::ComputationalBudgetExceeded);
4279        assert_eq!(parse_error_code(&err), None);
4280        assert_eq!(parse_instruction_index(&err), Some(4));
4281    }
4282
4283    #[test]
4284    fn parse_error_code_custom_one() {
4285        use solana_instruction::error::InstructionError;
4286        // Custom(1) — smallest non-zero
4287        let err = TransactionError::InstructionError(0, InstructionError::Custom(1));
4288        assert_eq!(parse_error_code(&err), Some(1));
4289    }
4290
4291    #[test]
4292    fn parse_both_from_same_error() {
4293        use solana_instruction::error::InstructionError;
4294        // Both error code and instruction index from a single error
4295        let err = TransactionError::InstructionError(7, InstructionError::Custom(9999));
4296        assert_eq!(parse_error_code(&err), Some(9999));
4297        assert_eq!(parse_instruction_index(&err), Some(7));
4298    }
4299
4300    #[test]
4301    fn tx_result_to_outcome_success() {
4302        use litesvm::types::TransactionMetadata;
4303        let meta = TransactionMetadata {
4304            compute_units_consumed: 42,
4305            logs: vec!["log".to_string()],
4306            ..Default::default()
4307        };
4308        let outcome = tx_result_to_outcome(Ok(meta));
4309        assert!(outcome.is_success());
4310        assert_eq!(outcome.compute_units(), Some(42));
4311        assert_eq!(outcome.error_code(), None);
4312    }
4313
4314    #[test]
4315    fn tx_result_to_outcome_error_sets_tls() {
4316        use litesvm::types::{FailedTransactionMetadata, TransactionMetadata};
4317        use solana_instruction::error::InstructionError;
4318        let failed = FailedTransactionMetadata {
4319            err: TransactionError::InstructionError(1, InstructionError::Custom(6051)),
4320            meta: TransactionMetadata {
4321                logs: vec!["err log".to_string()],
4322                ..Default::default()
4323            },
4324        };
4325        let outcome = tx_result_to_outcome(Err(failed));
4326        assert!(outcome.is_error());
4327        assert_eq!(outcome.error_code(), Some(6051));
4328        // TLS should have been set
4329        let tls_code = take_last_error_code();
4330        assert_eq!(tls_code, Some(6051));
4331    }
4332
4333    // =========================================================================
4334    // Snapshot — deeper edge cases
4335    // =========================================================================
4336
4337    #[test]
4338    fn test_snapshot_multiple_writes_same_account() {
4339        let mut ctx = TestContext::new();
4340        let pk = Pubkey::new_unique();
4341
4342        // Initial state
4343        ctx.write_account(
4344            &pk,
4345            Account {
4346                lamports: 100,
4347                data: vec![1],
4348                owner: Pubkey::new_unique(),
4349                executable: false,
4350                rent_epoch: 0,
4351            },
4352        )
4353        .unwrap();
4354        ctx.dirty_tracker.mark_account_dirty(&pk);
4355
4356        ctx.take_snapshot();
4357
4358        // Write #1 during iteration
4359        ctx.write_account(
4360            &pk,
4361            Account {
4362                lamports: 200,
4363                data: vec![2],
4364                owner: Pubkey::new_unique(),
4365                executable: false,
4366                rent_epoch: 0,
4367            },
4368        )
4369        .unwrap();
4370        ctx.dirty_tracker.mark_account_dirty(&pk);
4371
4372        // Write #2 during iteration
4373        ctx.write_account(
4374            &pk,
4375            Account {
4376                lamports: 300,
4377                data: vec![3],
4378                owner: Pubkey::new_unique(),
4379                executable: false,
4380                rent_epoch: 0,
4381            },
4382        )
4383        .unwrap();
4384        ctx.dirty_tracker.mark_account_dirty(&pk);
4385
4386        // Restore should go back to snapshot state (lamports=100, data=[1])
4387        ctx.restore_snapshot();
4388        let acc = ctx.svm.get_account(&pk).unwrap();
4389        assert_eq!(
4390            acc.lamports, 100,
4391            "restore should use snapshot value, not intermediate"
4392        );
4393        assert_eq!(acc.data, vec![1]);
4394    }
4395
4396    #[test]
4397    fn test_snapshot_large_account_data_integrity() {
4398        let mut ctx = TestContext::new();
4399        let pk = Pubkey::new_unique();
4400
4401        // Create a large account (10KB)
4402        let original_data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
4403        ctx.write_account(
4404            &pk,
4405            Account {
4406                lamports: 1_000_000,
4407                data: original_data.clone(),
4408                owner: Pubkey::new_unique(),
4409                executable: false,
4410                rent_epoch: 0,
4411            },
4412        )
4413        .unwrap();
4414        ctx.dirty_tracker.mark_account_dirty(&pk);
4415
4416        ctx.take_snapshot();
4417
4418        // Modify during iteration
4419        ctx.write_account(
4420            &pk,
4421            Account {
4422                lamports: 1_000_000,
4423                data: vec![0xFF; 10_000],
4424                owner: Pubkey::new_unique(),
4425                executable: false,
4426                rent_epoch: 0,
4427            },
4428        )
4429        .unwrap();
4430        ctx.dirty_tracker.mark_account_dirty(&pk);
4431
4432        ctx.restore_snapshot();
4433        let acc = ctx.svm.get_account(&pk).unwrap();
4434        assert_eq!(
4435            acc.data, original_data,
4436            "10KB data should be perfectly restored"
4437        );
4438    }
4439
4440    // -------------------------------------------------------------------------
4441    // add_program() FUZZ_PROGRAM_SO override tests
4442    // -------------------------------------------------------------------------
4443
4444    // Env var mutex: these tests manipulate FUZZ_PROGRAM_SO so must not run concurrently.
4445    use std::sync::Mutex;
4446    static ENV_MUTEX: Mutex<()> = Mutex::new(());
4447
4448    /// Find any valid SBF .so in the repo for testing.
4449    fn find_test_so() -> String {
4450        std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4451            .join("test-data/staking.so")
4452            .to_string_lossy()
4453            .into_owned()
4454    }
4455
4456    #[test]
4457    fn test_add_program_override_env_var() {
4458        let _lock = ENV_MUTEX.lock().unwrap();
4459        let so_path = find_test_so();
4460
4461        // Set override to the known-good .so
4462        std::env::set_var("FUZZ_PROGRAM_SO", &so_path);
4463
4464        let mut ctx = TestContext::new();
4465        let program_id = Pubkey::new_unique();
4466
4467        // Call with a bogus path — override should redirect to the real .so
4468        let result = ctx.add_program(&program_id, "/nonexistent/bogus.so");
4469        std::env::remove_var("FUZZ_PROGRAM_SO");
4470
4471        assert!(
4472            result.is_ok(),
4473            "Override should load from FUZZ_PROGRAM_SO, not the bogus path"
4474        );
4475    }
4476
4477    #[test]
4478    fn test_add_program_normal_path() {
4479        let _lock = ENV_MUTEX.lock().unwrap();
4480        std::env::remove_var("FUZZ_PROGRAM_SO");
4481
4482        let so_path = find_test_so();
4483        let mut ctx = TestContext::new();
4484        let program_id = Pubkey::new_unique();
4485
4486        let result = ctx.add_program(&program_id, &so_path);
4487        assert!(
4488            result.is_ok(),
4489            "Normal add_program should work without override"
4490        );
4491    }
4492
4493    #[test]
4494    fn test_add_program_override_nonexistent_errors() {
4495        let _lock = ENV_MUTEX.lock().unwrap();
4496
4497        // Point override to a nonexistent file
4498        std::env::set_var("FUZZ_PROGRAM_SO", "/tmp/does_not_exist_xyz.so");
4499
4500        let mut ctx = TestContext::new();
4501        let program_id = Pubkey::new_unique();
4502
4503        let result = ctx.add_program(&program_id, "/also/bogus.so");
4504        std::env::remove_var("FUZZ_PROGRAM_SO");
4505
4506        assert!(
4507            result.is_err(),
4508            "Override pointing to nonexistent file should error"
4509        );
4510    }
4511
4512    // -------------------------------------------------------------------------
4513    // Test ProgramBuilder
4514    // -------------------------------------------------------------------------
4515
4516    #[test]
4517    fn test_program_builder_fee_payer() {
4518        let mut ctx = TestContext::new();
4519        let program_id = Pubkey::new_unique();
4520        let fee_payer = Keypair::new();
4521        let signer = Keypair::new();
4522
4523        let builder = ctx
4524            .program(program_id)
4525            .fee_payer(&fee_payer)
4526            .signers(&[&signer]);
4527
4528        assert_eq!(builder.fee_payer, Some(fee_payer.insecure_clone()));
4529
4530        let builder = ctx
4531            .program(program_id)
4532            .signers(&[&signer])
4533            .fee_payer(&fee_payer);
4534
4535        assert_eq!(builder.fee_payer, Some(fee_payer));
4536    }
4537
4538    // =========================================================================
4539    // Change 1: TxOutcome new fields — signature, inner_instructions,
4540    //           return_data, fee are populated from litesvm metadata
4541    // =========================================================================
4542
4543    #[test]
4544    fn tx_result_to_outcome_success_preserves_all_metadata() {
4545        use litesvm::types::TransactionMetadata;
4546        use solana_message::compiled_instruction::CompiledInstruction;
4547        use solana_message::inner_instruction::InnerInstruction;
4548
4549        let sig = Signature::from([1u8; 64]);
4550        let return_program = Pubkey::new_unique();
4551        let inner_ix = InnerInstruction {
4552            instruction: CompiledInstruction {
4553                program_id_index: 2,
4554                accounts: vec![0, 1],
4555                data: vec![0xAB, 0xCD],
4556            },
4557            stack_height: 2,
4558        };
4559
4560        let meta = TransactionMetadata {
4561            signature: sig,
4562            logs: vec!["Program invoked".to_string()],
4563            inner_instructions: vec![vec![inner_ix]],
4564            compute_units_consumed: 12345,
4565            return_data: TransactionReturnData {
4566                program_id: return_program,
4567                data: vec![1, 2, 3, 4],
4568            },
4569            fee: 5000,
4570        };
4571
4572        let outcome = tx_result_to_outcome(Ok(meta));
4573
4574        // Verify all fields are preserved, not just compute_units and logs
4575        assert!(outcome.is_success());
4576        assert_eq!(outcome.compute_units(), Some(12345));
4577        assert_eq!(outcome.logs(), &["Program invoked"]);
4578        assert_eq!(*outcome.signature(), sig);
4579        assert_eq!(outcome.fee(), 5000);
4580        assert_eq!(outcome.return_data().program_id, return_program);
4581        assert_eq!(outcome.return_data().data, vec![1, 2, 3, 4]);
4582        assert_eq!(outcome.inner_instructions().len(), 1);
4583        assert_eq!(outcome.inner_instructions()[0].len(), 1);
4584        assert_eq!(outcome.inner_instructions()[0][0].stack_height, 2);
4585        assert_eq!(
4586            outcome.inner_instructions()[0][0].instruction.data,
4587            vec![0xAB, 0xCD]
4588        );
4589    }
4590
4591    #[test]
4592    fn tx_result_to_outcome_error_preserves_all_metadata() {
4593        use litesvm::types::{FailedTransactionMetadata, TransactionMetadata};
4594        use solana_instruction::error::InstructionError;
4595
4596        let sig = Signature::from([1u8; 64]);
4597        let failed = FailedTransactionMetadata {
4598            err: TransactionError::InstructionError(2, InstructionError::Custom(9999)),
4599            meta: TransactionMetadata {
4600                signature: sig,
4601                logs: vec!["Program failed".to_string()],
4602                inner_instructions: vec![vec![], vec![]],
4603                compute_units_consumed: 500,
4604                return_data: TransactionReturnData {
4605                    program_id: Pubkey::new_unique(),
4606                    data: vec![0xFF],
4607                },
4608                fee: 7500,
4609            },
4610        };
4611
4612        let outcome = tx_result_to_outcome(Err(failed));
4613
4614        assert!(outcome.is_error());
4615        assert_eq!(outcome.error_code(), Some(9999));
4616        assert_eq!(*outcome.signature(), sig);
4617        assert_eq!(outcome.fee(), 7500);
4618        assert_eq!(outcome.return_data().data, vec![0xFF]);
4619        assert_eq!(outcome.inner_instructions().len(), 2);
4620        assert_eq!(outcome.logs(), &["Program failed"]);
4621    }
4622
4623    #[test]
4624    fn tx_outcome_into_result_preserves_new_fields_in_tx_error() {
4625        let sig = Signature::from([1u8; 64]);
4626        let return_program = Pubkey::new_unique();
4627
4628        let error = TxOutcome::ProgramError {
4629            error: TransactionError::AccountInUse,
4630            error_code: Some(42),
4631            instruction_index: Some(3),
4632            logs: vec!["log".to_string()],
4633            signature: sig,
4634            inner_instructions: vec![vec![]],
4635            return_data: TransactionReturnData {
4636                program_id: return_program,
4637                data: vec![10, 20],
4638            },
4639            fee: 3000,
4640        };
4641
4642        let err = error.into_result().unwrap_err();
4643
4644        // All fields must survive the TxOutcome → TxError conversion
4645        assert_eq!(err.error_code, Some(42));
4646        assert_eq!(err.instruction_index, Some(3));
4647        assert_eq!(err.signature, sig);
4648        assert_eq!(err.fee, 3000);
4649        assert_eq!(err.return_data.program_id, return_program);
4650        assert_eq!(err.return_data.data, vec![10, 20]);
4651        assert_eq!(err.inner_instructions.len(), 1);
4652        assert_eq!(err.logs, vec!["log"]);
4653    }
4654
4655    #[test]
4656    fn tx_outcome_accessors_agree_across_variants() {
4657        // Both variants should return the same values through accessors
4658        let sig = Signature::from([1u8; 64]);
4659        let rd = TransactionReturnData {
4660            program_id: Pubkey::new_unique(),
4661            data: vec![42],
4662        };
4663
4664        let success = TxOutcome::Success {
4665            compute_units: 0,
4666            logs: vec![],
4667            signature: sig,
4668            inner_instructions: vec![],
4669            return_data: rd.clone(),
4670            fee: 100,
4671        };
4672
4673        let error = TxOutcome::ProgramError {
4674            error: TransactionError::AccountInUse,
4675            error_code: None,
4676            instruction_index: None,
4677            logs: vec![],
4678            signature: sig,
4679            inner_instructions: vec![],
4680            return_data: rd.clone(),
4681            fee: 100,
4682        };
4683
4684        assert_eq!(*success.signature(), *error.signature());
4685        assert_eq!(success.fee(), error.fee());
4686        assert_eq!(success.return_data().data, error.return_data().data);
4687        assert_eq!(
4688            success.inner_instructions().len(),
4689            error.inner_instructions().len()
4690        );
4691    }
4692
4693    // =========================================================================
4694    // Change 2: Arc-wrapped program_coverage_totals — shared across clones,
4695    //           only mutable during setup via Arc::make_mut
4696    // =========================================================================
4697
4698    #[test]
4699    fn program_coverage_totals_shared_across_clones() {
4700        let _lock = ENV_MUTEX.lock().unwrap();
4701        std::env::remove_var("FUZZ_PROGRAM_SO");
4702
4703        let so_path = find_test_so();
4704        let mut ctx = TestContext::new();
4705        let program_id = Pubkey::new_unique();
4706        ctx.add_program(&program_id, &so_path).unwrap();
4707
4708        // Verify totals were populated
4709        let totals = ctx.get_program_coverage_totals();
4710        assert!(
4711            totals.contains_key(&program_id),
4712            "program should have coverage totals"
4713        );
4714        let (edges, instrs) = totals[&program_id];
4715        assert!(edges > 0, "should have edges: {}", edges);
4716        assert!(instrs > 0, "should have instructions: {}", instrs);
4717
4718        // Clone and verify it's the same Arc (cheap pointer clone, not deep copy)
4719        let cloned = ctx.clone();
4720        let cloned_totals = cloned.get_program_coverage_totals();
4721        assert_eq!(
4722            cloned_totals[&program_id],
4723            (edges, instrs),
4724            "clone must have identical coverage totals"
4725        );
4726
4727        // Arc::ptr_eq would be ideal but we only have &HashMap. Verify via
4728        // the value equality at minimum — the type is Arc<HashMap> so clone
4729        // is O(1) pointer bump, not O(n) deep copy.
4730    }
4731
4732    #[test]
4733    fn program_coverage_totals_empty_without_program() {
4734        let ctx = TestContext::new();
4735        assert!(
4736            ctx.get_program_coverage_totals().is_empty(),
4737            "fresh context should have no coverage totals"
4738        );
4739
4740        let cloned = ctx.clone();
4741        assert!(
4742            cloned.get_program_coverage_totals().is_empty(),
4743            "clone of fresh context should also be empty"
4744        );
4745    }
4746
4747    // =========================================================================
4748    // Change 3: Entry-point-aware analyze_program_coverage — BFS from
4749    //           entrypoint + function entries, only counting reachable code
4750    // =========================================================================
4751
4752    #[test]
4753    fn analyze_program_coverage_returns_nonzero_for_valid_binary() {
4754        let _lock = ENV_MUTEX.lock().unwrap();
4755        std::env::remove_var("FUZZ_PROGRAM_SO");
4756
4757        let so_path = find_test_so();
4758        let program_data = std::fs::read(&so_path).unwrap();
4759
4760        let result = TestContext::analyze_program_coverage(&program_data);
4761        assert!(
4762            result.is_some(),
4763            "valid SBF binary should produce coverage data"
4764        );
4765
4766        let (edges, instructions) = result.unwrap();
4767        assert!(edges > 0, "should have conditional edges: {}", edges);
4768        assert!(
4769            instructions > 0,
4770            "should have instructions: {}",
4771            instructions
4772        );
4773
4774        // Sanity: edges should be less than instructions (not every instruction
4775        // is a conditional branch — conditional branches produce 2 edges each)
4776        assert!(
4777            edges < instructions * 2,
4778            "edges ({}) should be < 2*instructions ({})",
4779            edges,
4780            instructions
4781        );
4782    }
4783
4784    #[test]
4785    fn analyze_program_coverage_reachable_subset_of_total() {
4786        // The BFS-based analysis should count FEWER instructions than the total
4787        // number of instructions in the binary, because it excludes unreachable
4788        // code (linker stubs, unused library functions, etc.)
4789        use solana_sbpf::elf::Executable;
4790        use solana_sbpf::program::BuiltinProgram;
4791        use solana_sbpf::static_analysis::Analysis;
4792        use solana_sbpf::vm::ContextObject;
4793
4794        let _lock = ENV_MUTEX.lock().unwrap();
4795        std::env::remove_var("FUZZ_PROGRAM_SO");
4796
4797        let so_path = find_test_so();
4798        let program_data = std::fs::read(&so_path).unwrap();
4799
4800        // Get our BFS-based count
4801        let (bfs_edges, bfs_instructions) =
4802            TestContext::analyze_program_coverage(&program_data).unwrap();
4803
4804        // Get the raw total from the analysis (all nodes, including unreachable)
4805        struct DummyContext;
4806        impl ContextObject for DummyContext {
4807            fn consume(&mut self, _amount: u64) {}
4808            fn get_remaining(&self) -> u64 {
4809                0
4810            }
4811        }
4812        let loader = Arc::new(BuiltinProgram::<DummyContext>::new_mock());
4813        let executable = Executable::from_elf(&program_data, loader).unwrap();
4814        let analysis = Analysis::from_executable(&executable).unwrap();
4815        let total_all_instructions = analysis.instructions.len();
4816
4817        // BFS count should be <= total (strictly less if any dead code exists)
4818        assert!(
4819            bfs_instructions <= total_all_instructions,
4820            "BFS instructions ({}) should be <= total instructions ({})",
4821            bfs_instructions,
4822            total_all_instructions
4823        );
4824
4825        // For a real program binary there should be at least some unreachable
4826        // code (linker stubs, panic handlers, etc.)
4827        // We don't assert strictly less because small programs may have no dead code.
4828        eprintln!(
4829            "[TEST] BFS reachable: {} instructions, {} edges. Total in binary: {} instructions. \
4830             Excluded: {} instructions ({:.1}%)",
4831            bfs_instructions,
4832            bfs_edges,
4833            total_all_instructions,
4834            total_all_instructions - bfs_instructions,
4835            ((total_all_instructions - bfs_instructions) as f64 / total_all_instructions as f64)
4836                * 100.0
4837        );
4838    }
4839
4840    #[test]
4841    fn analyze_program_coverage_returns_none_for_garbage() {
4842        let garbage = vec![0u8; 64];
4843        assert!(
4844            TestContext::analyze_program_coverage(&garbage).is_none(),
4845            "garbage bytes should not parse as valid SBF"
4846        );
4847    }
4848
4849    #[test]
4850    fn analyze_program_coverage_returns_none_for_empty() {
4851        assert!(
4852            TestContext::analyze_program_coverage(&[]).is_none(),
4853            "empty bytes should not parse as valid SBF"
4854        );
4855    }
4856
4857    #[test]
4858    fn analyze_program_coverage_edges_only_from_conditional_jumps() {
4859        let _lock = ENV_MUTEX.lock().unwrap();
4860        std::env::remove_var("FUZZ_PROGRAM_SO");
4861
4862        let so_path = find_test_so();
4863        let program_data = std::fs::read(&so_path).unwrap();
4864
4865        let (edges, _) = TestContext::analyze_program_coverage(&program_data).unwrap();
4866
4867        // Edges come from conditional jumps, each producing 2 edges (taken/not-taken).
4868        // So edge count should always be even.
4869        assert_eq!(
4870            edges % 2,
4871            0,
4872            "edge count ({}) should be even (each conditional branch = 2 edges)",
4873            edges
4874        );
4875    }
4876
4877    // =========================================================================
4878    // Fee payer / signer resolution regression tests
4879    // =========================================================================
4880
4881    /// Helper: create a funded keypair in the test context
4882    fn fund_keypair(ctx: &mut TestContext) -> Keypair {
4883        let kp = Keypair::new();
4884        ctx.svm.airdrop(&kp.pubkey(), 10_000_000_000).unwrap();
4885        kp
4886    }
4887
4888    #[test]
4889    fn raw_call_with_signers_no_fee_payer() {
4890        // Regression: raw_call().signers(&[payer]).send() should use first signer as fee payer
4891        let mut ctx = TestContext::new();
4892        let payer = fund_keypair(&mut ctx);
4893        let recipient = Pubkey::new_unique();
4894
4895        let ix = anchor_lang::solana_program::system_instruction::transfer(
4896            &payer.pubkey(),
4897            &recipient,
4898            1000,
4899        );
4900        let result = ctx.raw_call(ix).signers(&[&payer]).send();
4901        assert!(
4902            result.is_ok(),
4903            "raw_call with signers should succeed: {:?}",
4904            result.err()
4905        );
4906        assert!(result.unwrap().is_success());
4907    }
4908
4909    #[test]
4910    fn raw_call_with_explicit_fee_payer() {
4911        // fee_payer() should be used even if it's not in signers list
4912        let mut ctx = TestContext::new();
4913        let payer = fund_keypair(&mut ctx);
4914        let recipient = Pubkey::new_unique();
4915
4916        let ix = anchor_lang::solana_program::system_instruction::transfer(
4917            &payer.pubkey(),
4918            &recipient,
4919            1000,
4920        );
4921        let result = ctx.raw_call(ix).fee_payer(&payer).send();
4922        assert!(
4923            result.is_ok(),
4924            "raw_call with fee_payer should succeed: {:?}",
4925            result.err()
4926        );
4927        assert!(result.unwrap().is_success());
4928    }
4929
4930    #[test]
4931    fn raw_call_fee_payer_prepended_to_signers() {
4932        // When fee_payer is set AND signers are set, fee_payer should be prepended
4933        let mut ctx = TestContext::new();
4934        let payer = fund_keypair(&mut ctx);
4935        let other_signer = fund_keypair(&mut ctx);
4936        let recipient = Pubkey::new_unique();
4937
4938        let ix = anchor_lang::solana_program::system_instruction::transfer(
4939            &payer.pubkey(),
4940            &recipient,
4941            1000,
4942        );
4943        let result = ctx
4944            .raw_call(ix)
4945            .fee_payer(&payer)
4946            .signers(&[&other_signer])
4947            .send();
4948        assert!(
4949            result.is_ok(),
4950            "fee_payer + signers should succeed: {:?}",
4951            result.err()
4952        );
4953    }
4954
4955    #[test]
4956    fn raw_call_no_signers_no_fee_payer_errors() {
4957        // No signers and no fee payer should error
4958        let mut ctx = TestContext::new();
4959        let ix = anchor_lang::solana_program::system_instruction::transfer(
4960            &Pubkey::new_unique(),
4961            &Pubkey::new_unique(),
4962            1000,
4963        );
4964        let result = ctx.raw_call(ix).send();
4965        assert!(result.is_err(), "should error with no signers");
4966    }
4967
4968    #[test]
4969    fn raw_call_duplicate_fee_payer_signer_no_double_sign() {
4970        // fee_payer same as first signer — should not duplicate
4971        let mut ctx = TestContext::new();
4972        let payer = fund_keypair(&mut ctx);
4973        let recipient = Pubkey::new_unique();
4974
4975        let ix = anchor_lang::solana_program::system_instruction::transfer(
4976            &payer.pubkey(),
4977            &recipient,
4978            1000,
4979        );
4980        let result = ctx.raw_call(ix).fee_payer(&payer).signers(&[&payer]).send();
4981        assert!(
4982            result.is_ok(),
4983            "duplicate fee_payer/signer should not cause double-sign error: {:?}",
4984            result.err()
4985        );
4986        assert!(result.unwrap().is_success());
4987    }
4988
4989    #[test]
4990    fn send_batch_with_signers() {
4991        // send_batch uses pending_signers[0] as fee payer
4992        let mut ctx = TestContext::new();
4993        let payer = fund_keypair(&mut ctx);
4994        let recipient = Pubkey::new_unique();
4995
4996        let ix = anchor_lang::solana_program::system_instruction::transfer(
4997            &payer.pubkey(),
4998            &recipient,
4999            2000,
5000        );
5001        ctx.pending_instructions.push(ix);
5002        ctx.pending_signers.push(payer.insecure_clone());
5003        let result = ctx.send_batch();
5004        assert!(
5005            result.is_ok(),
5006            "send_batch should succeed: {:?}",
5007            result.err()
5008        );
5009        let outcome = result.unwrap();
5010        assert!(outcome.is_some());
5011        assert!(outcome.unwrap().is_success());
5012        assert_eq!(ctx.svm.get_balance(&recipient).unwrap_or(0), 2000);
5013    }
5014
5015    // =========================================================================
5016    // Dummy signing regression tests
5017    // =========================================================================
5018
5019    #[test]
5020    fn sigverify_false_uses_dummy_signatures() {
5021        // Default sigverify=false should still execute transactions successfully
5022        let mut ctx = TestContext::new();
5023        assert!(!ctx.sigverify, "default sigverify should be false");
5024        let payer = fund_keypair(&mut ctx);
5025        let recipient = Pubkey::new_unique();
5026
5027        let ix = anchor_lang::solana_program::system_instruction::transfer(
5028            &payer.pubkey(),
5029            &recipient,
5030            1000,
5031        );
5032        let result = ctx.raw_call(ix).signers(&[&payer]).send();
5033        assert!(result.is_ok());
5034        assert!(result.unwrap().is_success());
5035    }
5036
5037    #[test]
5038    fn sigverify_true_uses_real_signatures() {
5039        let mut ctx = TestContext::new();
5040        ctx.sigverify = true;
5041        // Re-enable sigverify on the SVM too
5042        ctx.svm = ctx.svm.with_sigverify(true);
5043        let payer = fund_keypair(&mut ctx);
5044        let recipient = Pubkey::new_unique();
5045
5046        let ix = anchor_lang::solana_program::system_instruction::transfer(
5047            &payer.pubkey(),
5048            &recipient,
5049            1000,
5050        );
5051        let result = ctx.raw_call(ix).signers(&[&payer]).send();
5052        assert!(
5053            result.is_ok(),
5054            "real signing should work: {:?}",
5055            result.err()
5056        );
5057        assert!(result.unwrap().is_success());
5058    }
5059
5060    #[test]
5061    fn multiple_transactions_with_dummy_signing() {
5062        // Verify dummy signing works across multiple sequential transactions
5063        let mut ctx = TestContext::new();
5064        let payer = fund_keypair(&mut ctx);
5065        let recipient = Pubkey::new_unique();
5066
5067        for i in 0..5 {
5068            let ix = anchor_lang::solana_program::system_instruction::transfer(
5069                &payer.pubkey(),
5070                &recipient,
5071                1000,
5072            );
5073            let result = ctx.raw_call(ix).signers(&[&payer]).send();
5074            assert!(
5075                result.is_ok(),
5076                "tx {} should succeed: {:?}",
5077                i,
5078                result.err()
5079            );
5080            assert!(
5081                result.unwrap().is_success(),
5082                "tx {} should be successful",
5083                i
5084            );
5085        }
5086
5087        // Verify lamports moved
5088        let balance = ctx.svm.get_balance(&recipient).unwrap_or(0);
5089        assert_eq!(
5090            balance, 5000,
5091            "recipient should have 5000 lamports after 5 transfers"
5092        );
5093    }
5094
5095    // =========================================================================
5096    // Sysvar snapshot regression tests
5097    // =========================================================================
5098
5099    #[test]
5100    fn sysvar_snapshot_excludes_large_sysvars() {
5101        // SlotHistory (131KB) and SlotHashes (20KB) should NOT be in snapshots
5102        use anchor_lang::prelude::sysvar::SysvarId;
5103        use anchor_lang::prelude::Clock;
5104
5105        let ctx = TestContext::new();
5106        let snapshot = snapshot::SvmSnapshot::take_all(&ctx.svm);
5107
5108        let sysvar_pubkeys: Vec<_> = snapshot.sysvars.iter().map(|(pk, _)| *pk).collect();
5109
5110        // Clock should be present
5111        assert!(
5112            sysvar_pubkeys.contains(&Clock::id()),
5113            "Clock should be in snapshot"
5114        );
5115
5116        // SlotHistory and SlotHashes should NOT be present
5117        let slot_history_id =
5118            solana_pubkey::Pubkey::from_str_const("SysvarS1otHistory11111111111111111111111111");
5119        let slot_hashes_id =
5120            solana_pubkey::Pubkey::from_str_const("SysvarS1otHashes111111111111111111111111111");
5121        assert!(
5122            !sysvar_pubkeys.contains(&slot_history_id),
5123            "SlotHistory (131KB) should be excluded from snapshots"
5124        );
5125        assert!(
5126            !sysvar_pubkeys.contains(&slot_hashes_id),
5127            "SlotHashes (20KB) should be excluded from snapshots"
5128        );
5129    }
5130
5131    // =========================================================================
5132    // Clock target slot tracking regression tests
5133    // =========================================================================
5134
5135    #[test]
5136    fn advance_slots_records_target_slot() {
5137        let mut ctx = TestContext::new();
5138        ctx.advance_slots(500);
5139        assert_eq!(
5140            ctx.dirty_tracker.clock_target_slot,
5141            Some(500 + ctx.slot() - 500)
5142        );
5143        assert!(ctx.dirty_tracker.is_clock_dirty());
5144    }
5145
5146    #[test]
5147    fn warp_to_slot_records_target_slot() {
5148        let mut ctx = TestContext::new();
5149        ctx.warp_to_slot(12345);
5150        assert_eq!(ctx.dirty_tracker.clock_target_slot, Some(12345));
5151        assert!(ctx.dirty_tracker.is_clock_dirty());
5152    }
5153
5154    #[test]
5155    fn dirty_tracker_clear_resets_clock_target() {
5156        let mut ctx = TestContext::new();
5157        ctx.advance_slots(100);
5158        assert!(ctx.dirty_tracker.clock_target_slot.is_some());
5159        ctx.dirty_tracker.clear();
5160        assert!(ctx.dirty_tracker.clock_target_slot.is_none());
5161        assert!(!ctx.dirty_tracker.is_clock_dirty());
5162    }
5163
5164    // === Regression: corpus_loading flag suppresses monitor output ===
5165
5166    #[test]
5167    fn corpus_loading_flag_defaults_false() {
5168        assert!(!is_corpus_loading());
5169    }
5170
5171    #[test]
5172    fn corpus_loading_flag_roundtrip() {
5173        set_corpus_loading(true);
5174        assert!(is_corpus_loading());
5175        set_corpus_loading(false);
5176        assert!(!is_corpus_loading());
5177    }
5178}