Skip to main content

harn_vm/
harness.rs

1//! Capability handle threaded into every Harn script as the `harness`
2//! parameter of `main`.
3//!
4//! `Harness` is the Harn-language analog of an explicit-capability handle: a
5//! single value the runtime hands to a script's `main` so that stdio, clock,
6//! filesystem, environment, randomness, and network access become surface in
7//! the type system instead of ambient globals. Each sub-handle (`stdio`,
8//! `clock`, `fs`, `env`, `random`, `net`) is a distinct named type that
9//! anchors the surface for its capability slice.
10//!
11//! This module defines:
12//!   * The runtime [`Harness`] value (and six sub-handle wrappers).
13//!   * [`Harness::real`], the production constructor that wraps wall-clock
14//!     time, `tokio::fs`, `std::env`, `rand::thread_rng`, and `reqwest`. The
15//!     downstream migration tickets (E4.2-E4.4) populate the per-handle
16//!     method surface against this concrete state; the E4.2 stdio surface is
17//!     wired while filesystem, environment, randomness, and network methods
18//!     still land in later slices.
19//!   * [`VmHarness`], the compact `VmValue` payload that carries the same
20//!     state through the bytecode VM and distinguishes the root handle from
21//!     its sub-handles via [`HarnessKind`].
22
23use std::collections::{BTreeMap, VecDeque};
24use std::fmt;
25use std::rc::Rc;
26use std::sync::{Arc, Mutex};
27use std::time::Duration;
28
29use async_trait::async_trait;
30use harn_clock::{Clock, PausedClock, RealClock};
31use time::OffsetDateTime;
32
33/// Six capability slices exposed by a [`Harness`].
34///
35/// `Root` is the parent handle; the others are the typed sub-handles users
36/// reach through field access (`harness.stdio`, `harness.clock`, ...).
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum HarnessKind {
39    Root,
40    Stdio,
41    Clock,
42    Fs,
43    Env,
44    Random,
45    Net,
46}
47
48impl HarnessKind {
49    /// The Harn-language type name for this kind (`Harness`, `HarnessStdio`,
50    /// etc.). Used by the typechecker primitive registration and by
51    /// `VmValue::type_name`.
52    pub const fn type_name(self) -> &'static str {
53        match self {
54            HarnessKind::Root => "Harness",
55            HarnessKind::Stdio => "HarnessStdio",
56            HarnessKind::Clock => "HarnessClock",
57            HarnessKind::Fs => "HarnessFs",
58            HarnessKind::Env => "HarnessEnv",
59            HarnessKind::Random => "HarnessRandom",
60            HarnessKind::Net => "HarnessNet",
61        }
62    }
63
64    /// Field name a parent `Harness` exposes for this sub-handle (e.g. the
65    /// `stdio` in `harness.stdio`). Returns `None` for the root.
66    pub const fn field_name(self) -> Option<&'static str> {
67        match self {
68            HarnessKind::Root => None,
69            HarnessKind::Stdio => Some("stdio"),
70            HarnessKind::Clock => Some("clock"),
71            HarnessKind::Fs => Some("fs"),
72            HarnessKind::Env => Some("env"),
73            HarnessKind::Random => Some("random"),
74            HarnessKind::Net => Some("net"),
75        }
76    }
77
78    /// Parse the field name a script uses to reach a sub-handle.
79    pub fn from_field_name(name: &str) -> Option<Self> {
80        match name {
81            "stdio" => Some(HarnessKind::Stdio),
82            "clock" => Some(HarnessKind::Clock),
83            "fs" => Some(HarnessKind::Fs),
84            "env" => Some(HarnessKind::Env),
85            "random" => Some(HarnessKind::Random),
86            "net" => Some(HarnessKind::Net),
87            _ => None,
88        }
89    }
90
91    /// All six sub-handle kinds, in the canonical field order.
92    pub const SUB_HANDLES: &'static [HarnessKind] = &[
93        HarnessKind::Stdio,
94        HarnessKind::Clock,
95        HarnessKind::Fs,
96        HarnessKind::Env,
97        HarnessKind::Random,
98        HarnessKind::Net,
99    ];
100
101    /// Every kind a Harn-script type annotation may reference.
102    pub const ALL: &'static [HarnessKind] = &[
103        HarnessKind::Root,
104        HarnessKind::Stdio,
105        HarnessKind::Clock,
106        HarnessKind::Fs,
107        HarnessKind::Env,
108        HarnessKind::Random,
109        HarnessKind::Net,
110    ];
111}
112
113/// Shared, refcounted state backing every sub-handle of a single `Harness`.
114///
115/// Method implementations (in `crate::vm::methods::harness`) borrow this to
116/// reach the concrete OS-backed primitives. Wrapped in `Arc` so handles are
117/// `Send + Sync` for VM contexts that move work onto other tasks.
118#[derive(Debug)]
119pub struct HarnessInner {
120    clock: Arc<dyn Clock>,
121    mode: HarnessMode,
122}
123
124impl HarnessInner {
125    pub fn clock(&self) -> &Arc<dyn Clock> {
126        &self.clock
127    }
128
129    pub(crate) fn mode(&self) -> &HarnessMode {
130        &self.mode
131    }
132}
133
134#[derive(Debug)]
135pub(crate) enum HarnessMode {
136    Real,
137    Null(NullHarnessState),
138    Mock(Box<MockHarnessState>),
139}
140
141#[derive(Debug, Default)]
142pub(crate) struct NullHarnessState {
143    deny_events: Mutex<Vec<DenyEvent>>,
144}
145
146impl NullHarnessState {
147    pub(crate) fn record_deny(
148        &self,
149        sub_handle: HarnessKind,
150        method: &str,
151        args: &[crate::VmValue],
152    ) {
153        self.deny_events
154            .lock()
155            .expect("deny events poisoned")
156            .push(DenyEvent::new(
157                sub_handle,
158                method,
159                args.iter().map(crate::VmValue::display).collect(),
160            ));
161    }
162
163    pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
164        self.deny_events
165            .lock()
166            .expect("deny events poisoned")
167            .clone()
168    }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct DenyEvent {
173    pub sub_handle: HarnessKind,
174    pub method: String,
175    pub args: Vec<String>,
176}
177
178impl DenyEvent {
179    fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
180        Self {
181            sub_handle,
182            method: method.to_string(),
183            args,
184        }
185    }
186}
187
188#[derive(Debug)]
189pub(crate) struct MockHarnessState {
190    calls: Mutex<Vec<HarnessCall>>,
191    clock: Arc<PausedClock>,
192    env: BTreeMap<String, String>,
193    fs_reads: BTreeMap<String, Vec<u8>>,
194    net_gets: BTreeMap<String, String>,
195    random_u64: Mutex<VecDeque<u64>>,
196    stdin_lines: Mutex<VecDeque<String>>,
197    stdio: Mutex<String>,
198    stderr: Mutex<String>,
199}
200
201impl MockHarnessState {
202    pub(crate) fn record_call(
203        &self,
204        sub_handle: HarnessKind,
205        method: &str,
206        args: &[crate::VmValue],
207    ) {
208        self.calls
209            .lock()
210            .expect("calls poisoned")
211            .push(HarnessCall::new(
212                sub_handle,
213                method,
214                args.iter().map(crate::VmValue::display).collect(),
215            ));
216    }
217
218    pub(crate) fn calls(&self) -> Vec<HarnessCall> {
219        self.calls.lock().expect("calls poisoned").clone()
220    }
221
222    pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
223        self.env.get(key).map(String::as_str)
224    }
225
226    pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
227        self.fs_reads.get(path).map(Vec::as_slice)
228    }
229
230    pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
231        self.net_gets.get(url).map(String::as_str)
232    }
233
234    pub(crate) fn next_random_u64(&self) -> Option<u64> {
235        let mut values = self.random_u64.lock().expect("random values poisoned");
236        values.pop_front()
237    }
238
239    pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
240        self.clock.advance(duration);
241    }
242
243    pub(crate) fn push_stdio(&self, text: &str) {
244        self.stdio
245            .lock()
246            .expect("stdio buffer poisoned")
247            .push_str(text);
248    }
249
250    pub(crate) fn stdio(&self) -> String {
251        self.stdio.lock().expect("stdio buffer poisoned").clone()
252    }
253
254    pub(crate) fn push_stderr(&self, text: &str) {
255        self.stderr
256            .lock()
257            .expect("stderr buffer poisoned")
258            .push_str(text);
259    }
260
261    pub(crate) fn stderr(&self) -> String {
262        self.stderr.lock().expect("stderr buffer poisoned").clone()
263    }
264
265    pub(crate) fn pop_stdin_line(&self) -> Option<String> {
266        self.stdin_lines
267            .lock()
268            .expect("stdin queue poisoned")
269            .pop_front()
270    }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct HarnessCall {
275    pub sub_handle: HarnessKind,
276    pub method: String,
277    pub args: Vec<String>,
278}
279
280impl HarnessCall {
281    fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
282        Self {
283            sub_handle,
284            method: method.to_string(),
285            args,
286        }
287    }
288}
289
290#[derive(Debug)]
291pub struct MockHarnessBuilder {
292    clock: Arc<PausedClock>,
293    env: BTreeMap<String, String>,
294    fs_reads: BTreeMap<String, Vec<u8>>,
295    net_gets: BTreeMap<String, String>,
296    random_u64: Vec<u64>,
297    stdin_lines: Vec<String>,
298}
299
300impl MockHarnessBuilder {
301    fn new() -> Self {
302        Self {
303            clock: paused_clock_at_unix_ms(0),
304            env: BTreeMap::new(),
305            fs_reads: BTreeMap::new(),
306            net_gets: BTreeMap::new(),
307            random_u64: Vec::new(),
308            stdin_lines: Vec::new(),
309        }
310    }
311
312    pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
313        self.clock = paused_clock_at_unix_ms(unix_ms);
314        self
315    }
316
317    pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
318        self.clock = PausedClock::new(origin);
319        self
320    }
321
322    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
323        self.env.insert(key.into(), value.into());
324        self
325    }
326
327    pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
328        self.fs_reads.insert(path.into(), data.into());
329        self
330    }
331
332    pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
333        self.net_gets.insert(url.into(), body.into());
334        self
335    }
336
337    pub fn random_u64(mut self, value: u64) -> Self {
338        self.random_u64.push(value);
339        self
340    }
341
342    /// Queue a line that `harness.stdio.read_line()` or
343    /// `harness.stdio.prompt(...)` will return next. Lines are dequeued
344    /// FIFO; once the queue is empty subsequent reads surface EOF
345    /// (`nil` for the unstructured form, `{ok: false, status: "eof"}`
346    /// for the structured form).
347    pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
348        self.stdin_lines.push(line.into());
349        self
350    }
351
352    pub fn build(self) -> Harness {
353        let clock = self.clock;
354        Harness::with_mode(
355            clock.clone() as Arc<dyn Clock>,
356            HarnessMode::Mock(Box::new(MockHarnessState {
357                calls: Mutex::new(Vec::new()),
358                clock,
359                env: self.env,
360                fs_reads: self.fs_reads,
361                net_gets: self.net_gets,
362                random_u64: Mutex::new(self.random_u64.into()),
363                stdin_lines: Mutex::new(self.stdin_lines.into()),
364                stdio: Mutex::new(String::new()),
365                stderr: Mutex::new(String::new()),
366            })),
367        )
368    }
369}
370
371/// The runtime handle threaded into `main(harness: Harness)`.
372///
373/// Cheap to clone; sub-handles share the same `Arc` inner state.
374#[derive(Debug, Clone)]
375pub struct Harness {
376    inner: Arc<HarnessInner>,
377}
378
379impl Harness {
380    /// Build the production handle wired to wall-clock time. Filesystem,
381    /// environment, randomness, and network access are layered on by the
382    /// E4.2-E4.4 migration tickets; the constructor only needs to succeed
383    /// without panicking today (per the E4.1 exit criteria).
384    ///
385    /// The production clock is wrapped in [`MockAwareClock`] so existing
386    /// `mock_time(...)` / `advance_time(...)` test fixtures observe
387    /// `harness.clock.*` reads identically to the ambient builtins. The
388    /// shim is part of the E4.3-E4.6 migration window and goes away once
389    /// the ambient `mock_time` test utility is retired by E4.5.
390    pub fn real() -> Self {
391        Self::with_mode(
392            Arc::new(MockAwareClock::new(RealClock::new())),
393            HarnessMode::Real,
394        )
395    }
396
397    /// Build a deny-by-default test handle. Every sub-handle method records a
398    /// [`DenyEvent`] and fails with a categorized VM error.
399    pub fn null() -> Self {
400        Self::with_mode(
401            paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
402            HarnessMode::Null(NullHarnessState::default()),
403        )
404    }
405
406    /// Build a record/replay test handle backed by a paused clock.
407    pub fn mock() -> MockHarnessBuilder {
408        MockHarnessBuilder::new()
409    }
410
411    /// Build a handle wired to a caller-supplied clock. Most callers want
412    /// [`Self::test`] (which constructs the `PausedClock` for you);
413    /// reach for this when an existing `Arc<dyn Clock>` is already in
414    /// hand — e.g. a `RecordedClock` wrapper.
415    pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
416        Self::with_mode(clock, HarnessMode::Real)
417    }
418
419    fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
420        Self {
421            inner: Arc::new(HarnessInner { clock, mode }),
422        }
423    }
424
425    pub fn deny_events(&self) -> Vec<DenyEvent> {
426        match self.inner.mode() {
427            HarnessMode::Null(state) => state.deny_events(),
428            HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
429        }
430    }
431
432    pub fn calls(&self) -> Vec<HarnessCall> {
433        match self.inner.mode() {
434            HarnessMode::Mock(state) => state.calls(),
435            HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
436        }
437    }
438
439    pub fn captured_stdio(&self) -> String {
440        match self.inner.mode() {
441            HarnessMode::Mock(state) => state.stdio(),
442            HarnessMode::Real | HarnessMode::Null(_) => String::new(),
443        }
444    }
445
446    pub fn captured_stderr(&self) -> String {
447        match self.inner.mode() {
448            HarnessMode::Mock(state) => state.stderr(),
449            HarnessMode::Real | HarnessMode::Null(_) => String::new(),
450        }
451    }
452
453    /// Build a deterministic test handle wired to a fresh
454    /// [`PausedClock`] pinned at the Unix epoch.
455    ///
456    /// Returns the harness paired with the underlying `PausedClock` so
457    /// tests can drive virtual time through `PausedClock::advance`
458    /// while passing the same `Harness` value into the VM. The two
459    /// share the underlying `Arc<dyn Clock>`, so the harness reflects
460    /// every advance immediately.
461    ///
462    /// Pairs with [`PausedClock::advance`] / [`PausedClock::set`] — see
463    /// [`Self::with_paused_clock`] for picking a non-epoch origin.
464    pub fn test() -> (Self, Arc<PausedClock>) {
465        Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
466    }
467
468    /// Like [`Self::test`], but pins the paused clock's wall origin to
469    /// `origin`. Lets tests anchor virtual time to a meaningful date
470    /// without manually advancing past the epoch first.
471    pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
472        let paused = PausedClock::new(origin);
473        let as_dyn: Arc<dyn Clock> = paused.clone();
474        (Self::with_clock(as_dyn), paused)
475    }
476
477    /// Field access for `harness.stdio`.
478    pub fn stdio(&self) -> HarnessStdio {
479        HarnessStdio {
480            inner: Arc::clone(&self.inner),
481        }
482    }
483
484    /// Field access for `harness.clock`.
485    pub fn clock(&self) -> HarnessClock {
486        HarnessClock {
487            inner: Arc::clone(&self.inner),
488        }
489    }
490
491    /// Field access for `harness.fs`.
492    pub fn fs(&self) -> HarnessFs {
493        HarnessFs {
494            inner: Arc::clone(&self.inner),
495        }
496    }
497
498    /// Field access for `harness.env`.
499    pub fn env(&self) -> HarnessEnv {
500        HarnessEnv {
501            inner: Arc::clone(&self.inner),
502        }
503    }
504
505    /// Field access for `harness.random`.
506    pub fn random(&self) -> HarnessRandom {
507        HarnessRandom {
508            inner: Arc::clone(&self.inner),
509        }
510    }
511
512    /// Field access for `harness.net`.
513    pub fn net(&self) -> HarnessNet {
514        HarnessNet {
515            inner: Arc::clone(&self.inner),
516        }
517    }
518
519    /// Lower this handle into the `VmValue::Harness` payload.
520    pub fn into_vm_value(self) -> crate::value::VmValue {
521        crate::value::VmValue::Harness(VmHarness {
522            inner: self.inner,
523            kind: HarnessKind::Root,
524        })
525    }
526}
527
528fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
529    let nanos = (unix_ms as i128).saturating_mul(1_000_000);
530    let origin =
531        OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
532    PausedClock::new(origin)
533}
534
535pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
536    crate::VmValue::String(Rc::from(value.into()))
537}
538
539impl Default for Harness {
540    fn default() -> Self {
541        Self::real()
542    }
543}
544
545/// stdio sub-handle: `print`, `println`, `eprint`, `eprintln`, `prompt`,
546/// `read_line`.
547#[derive(Debug, Clone)]
548pub struct HarnessStdio {
549    inner: Arc<HarnessInner>,
550}
551
552/// clock sub-handle: `now`, `monotonic_now`, `sleep`.
553#[derive(Debug, Clone)]
554pub struct HarnessClock {
555    inner: Arc<HarnessInner>,
556}
557
558impl HarnessClock {
559    pub fn clock(&self) -> &Arc<dyn Clock> {
560        self.inner.clock()
561    }
562}
563
564/// fs sub-handle: `read_file`, `write_file`, `exists`, `list_dir`,
565/// `delete_file`, ...
566#[derive(Debug, Clone)]
567pub struct HarnessFs {
568    inner: Arc<HarnessInner>,
569}
570
571/// env sub-handle: `get`, `set`, `vars`.
572#[derive(Debug, Clone)]
573pub struct HarnessEnv {
574    inner: Arc<HarnessInner>,
575}
576
577/// random sub-handle: `gen_u64`, `gen_range`, `gen_f64`, ...
578#[derive(Debug, Clone)]
579pub struct HarnessRandom {
580    inner: Arc<HarnessInner>,
581}
582
583/// net sub-handle: `http_get`, `http_post`, ...
584#[derive(Debug, Clone)]
585pub struct HarnessNet {
586    inner: Arc<HarnessInner>,
587}
588
589macro_rules! sub_handle_inner {
590    ($($ty:ty),* $(,)?) => {
591        $(
592            impl $ty {
593                #[allow(dead_code)]
594                pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
595                    &self.inner
596                }
597            }
598        )*
599    };
600}
601sub_handle_inner!(
602    HarnessStdio,
603    HarnessFs,
604    HarnessEnv,
605    HarnessRandom,
606    HarnessNet,
607);
608
609impl HarnessClock {
610    #[allow(dead_code)]
611    pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
612        &self.inner
613    }
614}
615
616/// Compact `VmValue` payload for a `Harness` or any of its sub-handles.
617///
618/// All seven variants share one `Arc<HarnessInner>`; `kind` discriminates the
619/// surface the VM exposes for property access and method dispatch.
620#[derive(Clone)]
621pub struct VmHarness {
622    inner: Arc<HarnessInner>,
623    kind: HarnessKind,
624}
625
626impl VmHarness {
627    pub fn kind(&self) -> HarnessKind {
628        self.kind
629    }
630
631    pub fn type_name(&self) -> &'static str {
632        self.kind.type_name()
633    }
634
635    pub fn inner(&self) -> &Arc<HarnessInner> {
636        &self.inner
637    }
638
639    /// Get the sub-handle reached by a field name (`stdio`, `clock`, etc.).
640    /// Returns `None` when the receiver is itself a sub-handle or the field
641    /// is unknown.
642    pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
643        if self.kind != HarnessKind::Root {
644            return None;
645        }
646        let kind = HarnessKind::from_field_name(field)?;
647        Some(VmHarness {
648            inner: Arc::clone(&self.inner),
649            kind,
650        })
651    }
652}
653
654impl fmt::Debug for VmHarness {
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        f.debug_struct("VmHarness")
657            .field("kind", &self.kind)
658            .finish_non_exhaustive()
659    }
660}
661
662/// Clock wrapper that consults the crate-wide `clock_mock` thread-local
663/// before delegating to an inner [`Clock`]. Used by [`Harness::real`] so
664/// `harness.clock.*` reads honor `mock_time(...)` / `advance_time(...)`
665/// during the E4.3-E4.6 migration. New tests should prefer
666/// [`Harness::test`] / [`PausedClock`] directly.
667#[derive(Debug)]
668pub struct MockAwareClock<C: Clock + 'static> {
669    inner: C,
670}
671
672impl<C: Clock + 'static> MockAwareClock<C> {
673    pub fn new(inner: C) -> Self {
674        Self { inner }
675    }
676}
677
678#[async_trait]
679impl<C: Clock + 'static> Clock for MockAwareClock<C> {
680    fn now_utc(&self) -> OffsetDateTime {
681        if let Some(mock) = crate::clock_mock::active_mock_clock() {
682            return mock.now_utc();
683        }
684        self.inner.now_utc()
685    }
686
687    fn monotonic_ms(&self) -> i64 {
688        if let Some(mock) = crate::clock_mock::active_mock_clock() {
689            return mock.monotonic_ms();
690        }
691        self.inner.monotonic_ms()
692    }
693
694    async fn sleep(&self, duration: Duration) {
695        if duration.is_zero() {
696            return;
697        }
698        if let Some(mock) = crate::clock_mock::active_mock_clock() {
699            // Single-script tests under `mock_time(...)` rely on `sleep(...)`
700            // advancing the mock and returning immediately — the same
701            // semantics as the legacy ambient `sleep_ms` builtin. Waiting
702            // on `mock.sleep` would deadlock because nothing else is
703            // driving `advance(...)` in the same task.
704            mock.advance_std_sync(duration);
705            return;
706        }
707        self.inner.sleep(duration).await;
708    }
709
710    async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
711        if let Some(mock) = crate::clock_mock::active_mock_clock() {
712            let now = mock.now_utc();
713            if deadline > now {
714                if let Ok(delta) = Duration::try_from(deadline - now) {
715                    mock.advance_std_sync(delta);
716                }
717            }
718            return;
719        }
720        self.inner.sleep_until_utc(deadline).await;
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    #[test]
729    fn real_constructs_without_panic() {
730        let _harness = Harness::real();
731    }
732
733    #[test]
734    fn sub_handles_share_inner_state() {
735        let harness = Harness::real();
736        let stdio_inner = Arc::as_ptr(harness.stdio().inner());
737        let clock_inner = Arc::as_ptr(harness.clock().inner());
738        assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
739    }
740
741    #[test]
742    fn kinds_round_trip_through_field_names() {
743        for kind in HarnessKind::SUB_HANDLES {
744            let field = kind.field_name().unwrap();
745            assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
746        }
747        assert!(HarnessKind::from_field_name("nope").is_none());
748        assert!(HarnessKind::Root.field_name().is_none());
749    }
750
751    #[test]
752    fn vm_harness_property_access_returns_sub_handle() {
753        let root = match Harness::real().into_vm_value() {
754            crate::value::VmValue::Harness(h) => h,
755            other => panic!("expected Harness variant, got {}", other.type_name()),
756        };
757        let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
758        assert_eq!(stdio.kind(), HarnessKind::Stdio);
759        assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
760        assert!(root.sub_handle("not_a_field").is_none());
761    }
762
763    #[test]
764    fn test_constructor_clock_advances_under_paused_clock_advance() {
765        let (harness, paused) = Harness::test();
766        let clock = harness.clock();
767        let start_wall = clock.clock().now_utc();
768        assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
769        assert_eq!(clock.clock().monotonic_ms(), 0);
770
771        paused.advance(Duration::from_millis(1_500));
772        assert_eq!(clock.clock().monotonic_ms(), 1_500);
773        let after_wall = clock.clock().now_utc();
774        assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
775    }
776
777    #[test]
778    fn with_paused_clock_pins_origin() {
779        let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
780        let (harness, paused) = Harness::with_paused_clock(origin);
781        assert_eq!(harness.clock().clock().now_utc(), origin);
782        paused.advance(Duration::from_secs(60));
783        assert_eq!(
784            harness.clock().clock().now_utc() - origin,
785            time::Duration::seconds(60)
786        );
787    }
788
789    #[test]
790    fn null_harness_records_deny_events_for_every_sub_handle() {
791        let harness = Harness::null();
792        for source in [
793            r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
794            r#"fn main(harness: Harness) { harness.clock.now_ms() }"#,
795            r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
796            r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
797            r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
798            r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
799        ] {
800            let error = run_harness_source(source, harness.clone()).expect_err("call denied");
801            assert!(
802                error.contains("NullHarness denied"),
803                "unexpected deny error: {error}"
804            );
805        }
806
807        let events = harness.deny_events();
808        let observed: Vec<_> = events
809            .iter()
810            .map(|event| (event.sub_handle, event.method.as_str()))
811            .collect();
812        assert_eq!(
813            observed,
814            vec![
815                (HarnessKind::Stdio, "println"),
816                (HarnessKind::Clock, "now_ms"),
817                (HarnessKind::Fs, "read_text"),
818                (HarnessKind::Env, "get"),
819                (HarnessKind::Random, "gen_u64"),
820                (HarnessKind::Net, "get"),
821            ]
822        );
823        assert_eq!(events[0].args, vec!["blocked"]);
824        assert_eq!(events[2].args, vec!["/x"]);
825    }
826
827    #[test]
828    fn mock_harness_replays_canned_responses_and_records_calls() {
829        let harness = Harness::mock()
830            .clock_at_unix_ms(1_700_000_000_000)
831            .env("KEY", "value")
832            .fs_read("/x", b"data".to_vec())
833            .random_u64(42)
834            .net_get("https://example.test", "body")
835            .build();
836
837        let output = run_harness_source(
838            r#"
839fn main(harness: Harness) {
840  harness.stdio.print("partial ")
841  harness.stdio.println("line")
842  println(harness.clock.now_ms())
843  harness.clock.sleep_ms(250)
844  println(harness.clock.now_ms())
845  println(harness.clock.monotonic_ms())
846  println(harness.env.get("KEY"))
847  println(harness.fs.read_text("/x"))
848  println(harness.fs.exists("/missing"))
849  println(harness.random.gen_u64())
850  println(harness.net.get("https://example.test"))
851}
852"#,
853            harness.clone(),
854        )
855        .expect("mock harness run succeeds");
856
857        assert_eq!(harness.captured_stdio(), "partial line\n");
858        assert_eq!(
859            output,
860            "1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\n"
861        );
862        let observed: Vec<_> = harness
863            .calls()
864            .into_iter()
865            .map(|call| (call.sub_handle, call.method))
866            .collect();
867        assert_eq!(
868            observed,
869            vec![
870                (HarnessKind::Stdio, "print".to_string()),
871                (HarnessKind::Stdio, "println".to_string()),
872                (HarnessKind::Clock, "now_ms".to_string()),
873                (HarnessKind::Clock, "sleep_ms".to_string()),
874                (HarnessKind::Clock, "now_ms".to_string()),
875                (HarnessKind::Clock, "monotonic_ms".to_string()),
876                (HarnessKind::Env, "get".to_string()),
877                (HarnessKind::Fs, "read_text".to_string()),
878                (HarnessKind::Fs, "exists".to_string()),
879                (HarnessKind::Random, "gen_u64".to_string()),
880                (HarnessKind::Net, "get".to_string()),
881            ]
882        );
883    }
884
885    #[test]
886    fn mock_harness_replays_random_values_fifo() {
887        let harness = Harness::mock()
888            .random_u64(7)
889            .random_u64(11)
890            .random_u64(u64::MAX)
891            .build();
892
893        let output = run_harness_source(
894            r#"
895fn main(harness: Harness) {
896  println(harness.random.gen_u64())
897  println(harness.random.gen_u64())
898  println(harness.random.gen_u64())
899}
900"#,
901            harness,
902        )
903        .expect("mock random succeeds");
904
905        assert_eq!(output, "7\n11\n9223372036854775807\n");
906    }
907
908    #[test]
909    fn mock_harness_reports_missing_canned_responses() {
910        let cases = [
911            (
912                r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
913                "MockHarness has no fs_read response for /missing",
914            ),
915            (
916                r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
917                "MockHarness has no random_u64 response",
918            ),
919            (
920                r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
921                "MockHarness has no net_get response for https://missing.test",
922            ),
923        ];
924
925        for (source, expected) in cases {
926            let error = run_harness_source(source, Harness::mock().build())
927                .expect_err("missing mock response fails");
928            assert!(
929                error.contains(expected),
930                "expected `{expected}` in `{error}`"
931            );
932        }
933    }
934
935    #[test]
936    fn mock_harness_records_failed_calls() {
937        let harness = Harness::mock().build();
938        let error = run_harness_source(
939            r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
940            harness.clone(),
941        )
942        .expect_err("missing mock response fails");
943
944        assert!(error.contains("MockHarness has no net_get response"));
945        assert_eq!(
946            harness.calls(),
947            vec![HarnessCall {
948                sub_handle: HarnessKind::Net,
949                method: "get".to_string(),
950                args: vec!["https://missing.test".to_string()],
951            }]
952        );
953    }
954
955    #[test]
956    fn mock_harness_captures_stderr_separately_from_stdout() {
957        let harness = Harness::mock().build();
958        run_harness_source(
959            r#"
960fn main(harness: Harness) {
961  harness.stdio.println("stdout line")
962  harness.stdio.eprint("err ")
963  harness.stdio.eprintln("trail")
964}
965"#,
966            harness.clone(),
967        )
968        .expect("stderr capture run succeeds");
969        assert_eq!(harness.captured_stdio(), "stdout line\n");
970        assert_eq!(harness.captured_stderr(), "err trail\n");
971    }
972
973    #[test]
974    fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
975        let harness = Harness::mock()
976            .stdin_line("first")
977            .stdin_line("second")
978            .build();
979        let output = run_harness_source(
980            r#"
981fn main(harness: Harness) {
982  harness.stdio.println(harness.stdio.read_line())
983  harness.stdio.println(harness.stdio.prompt("answer: "))
984  let eof = harness.stdio.read_line({trim: false})
985  harness.stdio.println(eof.status)
986}
987"#,
988            harness.clone(),
989        )
990        .expect("stdin replay succeeds");
991        // All stdio writes route to the mock capture buffer; vm.output stays empty.
992        assert_eq!(output, "");
993        assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
994    }
995
996    #[test]
997    fn mock_harness_rejects_wrong_argument_types() {
998        let error = run_harness_source(
999            r#"fn main(harness: Harness) { harness.fs.read_text(1) }"#,
1000            Harness::mock().build(),
1001        )
1002        .expect_err("wrong argument type fails");
1003
1004        assert!(error.contains("HarnessFs.read_text expects string argument 1, got int"));
1005    }
1006
1007    fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1008        let rt = tokio::runtime::Builder::new_current_thread()
1009            .enable_all()
1010            .build()
1011            .unwrap();
1012        rt.block_on(async move {
1013            let local = tokio::task::LocalSet::new();
1014            local
1015                .run_until(async move {
1016                    let chunk = crate::compile_source(source)?;
1017                    let mut vm = crate::Vm::new();
1018                    crate::stdlib::register_vm_stdlib(&mut vm);
1019                    vm.set_harness(harness);
1020                    vm.execute(&chunk)
1021                        .await
1022                        .map_err(|error| error.to_string())?;
1023                    Ok(vm.output().to_string())
1024                })
1025                .await
1026        })
1027    }
1028}