1use 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#[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 System,
47}
48
49impl HarnessKind {
50 pub const fn type_name(self) -> &'static str {
54 match self {
55 HarnessKind::Root => "Harness",
56 HarnessKind::Stdio => "HarnessStdio",
57 HarnessKind::Clock => "HarnessClock",
58 HarnessKind::Fs => "HarnessFs",
59 HarnessKind::Env => "HarnessEnv",
60 HarnessKind::Random => "HarnessRandom",
61 HarnessKind::Net => "HarnessNet",
62 HarnessKind::System => "HarnessSystem",
63 }
64 }
65
66 pub const fn field_name(self) -> Option<&'static str> {
69 match self {
70 HarnessKind::Root => None,
71 HarnessKind::Stdio => Some("stdio"),
72 HarnessKind::Clock => Some("clock"),
73 HarnessKind::Fs => Some("fs"),
74 HarnessKind::Env => Some("env"),
75 HarnessKind::Random => Some("random"),
76 HarnessKind::Net => Some("net"),
77 HarnessKind::System => Some("system"),
78 }
79 }
80
81 pub fn from_field_name(name: &str) -> Option<Self> {
83 match name {
84 "stdio" => Some(HarnessKind::Stdio),
85 "clock" => Some(HarnessKind::Clock),
86 "fs" => Some(HarnessKind::Fs),
87 "env" => Some(HarnessKind::Env),
88 "random" => Some(HarnessKind::Random),
89 "net" => Some(HarnessKind::Net),
90 "system" => Some(HarnessKind::System),
91 _ => None,
92 }
93 }
94
95 pub const SUB_HANDLES: &'static [HarnessKind] = &[
97 HarnessKind::Stdio,
98 HarnessKind::Clock,
99 HarnessKind::Fs,
100 HarnessKind::Env,
101 HarnessKind::Random,
102 HarnessKind::Net,
103 HarnessKind::System,
104 ];
105
106 pub const ALL: &'static [HarnessKind] = &[
108 HarnessKind::Root,
109 HarnessKind::Stdio,
110 HarnessKind::Clock,
111 HarnessKind::Fs,
112 HarnessKind::Env,
113 HarnessKind::Random,
114 HarnessKind::Net,
115 HarnessKind::System,
116 ];
117}
118
119#[derive(Debug)]
125pub struct HarnessInner {
126 clock: Arc<dyn Clock>,
127 mode: HarnessMode,
128 net_policy: Option<crate::harness_net::NetPolicy>,
133 quarantined: Mutex<bool>,
140}
141
142impl HarnessInner {
143 pub fn clock(&self) -> &Arc<dyn Clock> {
144 &self.clock
145 }
146
147 pub(crate) fn mode(&self) -> &HarnessMode {
148 &self.mode
149 }
150
151 pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
152 self.net_policy.as_ref()
153 }
154
155 pub(crate) fn mark_quarantined(&self) {
156 if let Ok(mut guard) = self.quarantined.lock() {
157 *guard = true;
158 }
159 }
160
161 pub fn is_quarantined(&self) -> bool {
162 self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
163 }
164}
165
166#[derive(Debug)]
167pub(crate) enum HarnessMode {
168 Real,
169 Null(NullHarnessState),
170 Mock(Arc<MockHarnessState>),
171}
172
173#[derive(Debug, Default)]
174pub(crate) struct NullHarnessState {
175 deny_events: Mutex<Vec<DenyEvent>>,
176}
177
178impl NullHarnessState {
179 pub(crate) fn record_deny(
180 &self,
181 sub_handle: HarnessKind,
182 method: &str,
183 args: &[crate::VmValue],
184 ) {
185 self.deny_events
186 .lock()
187 .expect("deny events poisoned")
188 .push(DenyEvent::new(
189 sub_handle,
190 method,
191 args.iter().map(crate::VmValue::display).collect(),
192 ));
193 }
194
195 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
196 self.deny_events
197 .lock()
198 .expect("deny events poisoned")
199 .clone()
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct DenyEvent {
205 pub sub_handle: HarnessKind,
206 pub method: String,
207 pub args: Vec<String>,
208}
209
210impl DenyEvent {
211 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
212 Self {
213 sub_handle,
214 method: method.to_string(),
215 args,
216 }
217 }
218}
219
220#[derive(Debug)]
221pub(crate) struct MockHarnessState {
222 calls: Mutex<Vec<HarnessCall>>,
223 clock: Arc<PausedClock>,
224 env: BTreeMap<String, String>,
225 fs_reads: BTreeMap<String, Vec<u8>>,
226 net_gets: BTreeMap<String, String>,
227 random_u64: Mutex<VecDeque<u64>>,
228 stdin_lines: Mutex<VecDeque<String>>,
229 stdio: Mutex<String>,
230 stderr: Mutex<String>,
231}
232
233impl MockHarnessState {
234 pub(crate) fn record_call(
235 &self,
236 sub_handle: HarnessKind,
237 method: &str,
238 args: &[crate::VmValue],
239 ) {
240 self.calls
241 .lock()
242 .expect("calls poisoned")
243 .push(HarnessCall::new(
244 sub_handle,
245 method,
246 args.iter().map(crate::VmValue::display).collect(),
247 ));
248 }
249
250 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
251 self.calls.lock().expect("calls poisoned").clone()
252 }
253
254 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
255 self.env.get(key).map(String::as_str)
256 }
257
258 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
259 self.fs_reads.get(path).map(Vec::as_slice)
260 }
261
262 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
263 self.net_gets.get(url).map(String::as_str)
264 }
265
266 pub(crate) fn next_random_u64(&self) -> Option<u64> {
267 let mut values = self.random_u64.lock().expect("random values poisoned");
268 values.pop_front()
269 }
270
271 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
272 self.clock.advance(duration);
273 }
274
275 pub(crate) fn push_stdio(&self, text: &str) {
276 self.stdio
277 .lock()
278 .expect("stdio buffer poisoned")
279 .push_str(text);
280 }
281
282 pub(crate) fn stdio(&self) -> String {
283 self.stdio.lock().expect("stdio buffer poisoned").clone()
284 }
285
286 pub(crate) fn push_stderr(&self, text: &str) {
287 self.stderr
288 .lock()
289 .expect("stderr buffer poisoned")
290 .push_str(text);
291 }
292
293 pub(crate) fn stderr(&self) -> String {
294 self.stderr.lock().expect("stderr buffer poisoned").clone()
295 }
296
297 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
298 self.stdin_lines
299 .lock()
300 .expect("stdin queue poisoned")
301 .pop_front()
302 }
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct HarnessCall {
307 pub sub_handle: HarnessKind,
308 pub method: String,
309 pub args: Vec<String>,
310}
311
312impl HarnessCall {
313 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
314 Self {
315 sub_handle,
316 method: method.to_string(),
317 args,
318 }
319 }
320}
321
322#[derive(Debug)]
323pub struct MockHarnessBuilder {
324 clock: Arc<PausedClock>,
325 env: BTreeMap<String, String>,
326 fs_reads: BTreeMap<String, Vec<u8>>,
327 net_gets: BTreeMap<String, String>,
328 random_u64: Vec<u64>,
329 stdin_lines: Vec<String>,
330}
331
332impl MockHarnessBuilder {
333 fn new() -> Self {
334 Self {
335 clock: paused_clock_at_unix_ms(0),
336 env: BTreeMap::new(),
337 fs_reads: BTreeMap::new(),
338 net_gets: BTreeMap::new(),
339 random_u64: Vec::new(),
340 stdin_lines: Vec::new(),
341 }
342 }
343
344 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
345 self.clock = paused_clock_at_unix_ms(unix_ms);
346 self
347 }
348
349 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
350 self.clock = PausedClock::new(origin);
351 self
352 }
353
354 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
355 self.env.insert(key.into(), value.into());
356 self
357 }
358
359 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
360 self.fs_reads.insert(path.into(), data.into());
361 self
362 }
363
364 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
365 self.net_gets.insert(url.into(), body.into());
366 self
367 }
368
369 pub fn random_u64(mut self, value: u64) -> Self {
370 self.random_u64.push(value);
371 self
372 }
373
374 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
380 self.stdin_lines.push(line.into());
381 self
382 }
383
384 pub fn build(self) -> Harness {
385 let clock = self.clock;
386 Harness::with_mode(
387 clock.clone() as Arc<dyn Clock>,
388 HarnessMode::Mock(Arc::new(MockHarnessState {
389 calls: Mutex::new(Vec::new()),
390 clock,
391 env: self.env,
392 fs_reads: self.fs_reads,
393 net_gets: self.net_gets,
394 random_u64: Mutex::new(self.random_u64.into()),
395 stdin_lines: Mutex::new(self.stdin_lines.into()),
396 stdio: Mutex::new(String::new()),
397 stderr: Mutex::new(String::new()),
398 })),
399 )
400 }
401}
402
403#[derive(Debug, Clone)]
407pub struct Harness {
408 inner: Arc<HarnessInner>,
409}
410
411impl Harness {
412 pub fn real() -> Self {
423 Self::with_mode(
424 Arc::new(MockAwareClock::new(RealClock::new())),
425 HarnessMode::Real,
426 )
427 }
428
429 pub fn null() -> Self {
432 Self::with_mode(
433 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
434 HarnessMode::Null(NullHarnessState::default()),
435 )
436 }
437
438 pub fn mock() -> MockHarnessBuilder {
440 MockHarnessBuilder::new()
441 }
442
443 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
448 Self::with_mode(clock, HarnessMode::Real)
449 }
450
451 pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
456 Self { inner }
457 }
458
459 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
460 #[allow(clippy::arc_with_non_send_sync)]
468 let inner = Arc::new(HarnessInner {
469 clock,
470 mode,
471 net_policy: None,
472 quarantined: Mutex::new(false),
473 });
474 Self { inner }
475 }
476
477 pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
491 let clock = Arc::clone(&self.inner.clock);
492 let mode = match &self.inner.mode {
493 HarnessMode::Real => HarnessMode::Real,
494 HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
495 HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
496 };
497 #[allow(clippy::arc_with_non_send_sync)]
499 let inner = Arc::new(HarnessInner {
500 clock,
501 mode,
502 net_policy: Some(policy),
503 quarantined: Mutex::new(self.is_quarantined()),
504 });
505 Self { inner }
506 }
507
508 pub fn is_quarantined(&self) -> bool {
511 self.inner.is_quarantined()
512 }
513
514 pub fn deny_events(&self) -> Vec<DenyEvent> {
515 match self.inner.mode() {
516 HarnessMode::Null(state) => state.deny_events(),
517 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
518 }
519 }
520
521 pub fn calls(&self) -> Vec<HarnessCall> {
522 match self.inner.mode() {
523 HarnessMode::Mock(state) => state.calls(),
524 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
525 }
526 }
527
528 pub fn captured_stdio(&self) -> String {
529 match self.inner.mode() {
530 HarnessMode::Mock(state) => state.stdio(),
531 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
532 }
533 }
534
535 pub fn captured_stderr(&self) -> String {
536 match self.inner.mode() {
537 HarnessMode::Mock(state) => state.stderr(),
538 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
539 }
540 }
541
542 pub fn test() -> (Self, Arc<PausedClock>) {
554 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
555 }
556
557 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
561 let paused = PausedClock::new(origin);
562 let as_dyn: Arc<dyn Clock> = paused.clone();
563 (Self::with_clock(as_dyn), paused)
564 }
565
566 pub fn stdio(&self) -> HarnessStdio {
568 HarnessStdio {
569 inner: Arc::clone(&self.inner),
570 }
571 }
572
573 pub fn clock(&self) -> HarnessClock {
575 HarnessClock {
576 inner: Arc::clone(&self.inner),
577 }
578 }
579
580 pub fn fs(&self) -> HarnessFs {
582 HarnessFs {
583 inner: Arc::clone(&self.inner),
584 }
585 }
586
587 pub fn env(&self) -> HarnessEnv {
589 HarnessEnv {
590 inner: Arc::clone(&self.inner),
591 }
592 }
593
594 pub fn random(&self) -> HarnessRandom {
596 HarnessRandom {
597 inner: Arc::clone(&self.inner),
598 }
599 }
600
601 pub fn net(&self) -> HarnessNet {
603 HarnessNet {
604 inner: Arc::clone(&self.inner),
605 }
606 }
607
608 pub fn system(&self) -> HarnessSystem {
610 HarnessSystem {
611 inner: Arc::clone(&self.inner),
612 }
613 }
614
615 pub fn into_vm_value(self) -> crate::value::VmValue {
617 crate::value::VmValue::harness(VmHarness {
618 inner: self.inner,
619 kind: HarnessKind::Root,
620 })
621 }
622}
623
624fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
625 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
626 let origin =
627 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
628 PausedClock::new(origin)
629}
630
631pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
632 crate::VmValue::String(Rc::from(value.into()))
633}
634
635impl Default for Harness {
636 fn default() -> Self {
637 Self::real()
638 }
639}
640
641#[derive(Debug, Clone)]
644pub struct HarnessStdio {
645 inner: Arc<HarnessInner>,
646}
647
648#[derive(Debug, Clone)]
650pub struct HarnessClock {
651 inner: Arc<HarnessInner>,
652}
653
654impl HarnessClock {
655 pub fn clock(&self) -> &Arc<dyn Clock> {
656 self.inner.clock()
657 }
658}
659
660#[derive(Debug, Clone)]
663pub struct HarnessFs {
664 inner: Arc<HarnessInner>,
665}
666
667#[derive(Debug, Clone)]
669pub struct HarnessEnv {
670 inner: Arc<HarnessInner>,
671}
672
673#[derive(Debug, Clone)]
675pub struct HarnessRandom {
676 inner: Arc<HarnessInner>,
677}
678
679#[derive(Debug, Clone)]
681pub struct HarnessNet {
682 inner: Arc<HarnessInner>,
683}
684
685#[derive(Debug, Clone)]
691pub struct HarnessSystem {
692 inner: Arc<HarnessInner>,
693}
694
695macro_rules! sub_handle_inner {
696 ($($ty:ty),* $(,)?) => {
697 $(
698 impl $ty {
699 #[allow(dead_code)]
700 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
701 &self.inner
702 }
703 }
704 )*
705 };
706}
707sub_handle_inner!(
708 HarnessStdio,
709 HarnessFs,
710 HarnessEnv,
711 HarnessRandom,
712 HarnessNet,
713 HarnessSystem,
714);
715
716impl HarnessClock {
717 #[allow(dead_code)]
718 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
719 &self.inner
720 }
721}
722
723#[derive(Clone)]
728pub struct VmHarness {
729 inner: Arc<HarnessInner>,
730 kind: HarnessKind,
731}
732
733impl VmHarness {
734 pub fn kind(&self) -> HarnessKind {
735 self.kind
736 }
737
738 pub fn type_name(&self) -> &'static str {
739 self.kind.type_name()
740 }
741
742 pub fn inner(&self) -> &Arc<HarnessInner> {
743 &self.inner
744 }
745
746 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
750 if self.kind != HarnessKind::Root {
751 return None;
752 }
753 let kind = HarnessKind::from_field_name(field)?;
754 Some(VmHarness {
755 inner: Arc::clone(&self.inner),
756 kind,
757 })
758 }
759}
760
761impl fmt::Debug for VmHarness {
762 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
763 f.debug_struct("VmHarness")
764 .field("kind", &self.kind)
765 .finish_non_exhaustive()
766 }
767}
768
769#[derive(Debug)]
775pub struct MockAwareClock<C: Clock + 'static> {
776 inner: C,
777}
778
779impl<C: Clock + 'static> MockAwareClock<C> {
780 pub fn new(inner: C) -> Self {
781 Self { inner }
782 }
783}
784
785#[async_trait]
786impl<C: Clock + 'static> Clock for MockAwareClock<C> {
787 fn now_utc(&self) -> OffsetDateTime {
788 if let Some(mock) = crate::clock_mock::active_mock_clock() {
789 return mock.now_utc();
790 }
791 self.inner.now_utc()
792 }
793
794 fn monotonic_ms(&self) -> i64 {
795 if let Some(mock) = crate::clock_mock::active_mock_clock() {
796 return mock.monotonic_ms();
797 }
798 self.inner.monotonic_ms()
799 }
800
801 async fn sleep(&self, duration: Duration) {
802 if duration.is_zero() {
803 return;
804 }
805 if let Some(mock) = crate::clock_mock::active_mock_clock() {
806 mock.advance_std_sync(duration);
812 return;
813 }
814 self.inner.sleep(duration).await;
815 }
816
817 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
818 if let Some(mock) = crate::clock_mock::active_mock_clock() {
819 let now = mock.now_utc();
820 if deadline > now {
821 if let Ok(delta) = Duration::try_from(deadline - now) {
822 mock.advance_std_sync(delta);
823 }
824 }
825 return;
826 }
827 self.inner.sleep_until_utc(deadline).await;
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 #[test]
836 fn real_constructs_without_panic() {
837 let _harness = Harness::real();
838 }
839
840 #[test]
841 fn sub_handles_share_inner_state() {
842 let harness = Harness::real();
843 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
844 let clock_inner = Arc::as_ptr(harness.clock().inner());
845 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
846 }
847
848 #[test]
849 fn kinds_round_trip_through_field_names() {
850 for kind in HarnessKind::SUB_HANDLES {
851 let field = kind.field_name().unwrap();
852 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
853 }
854 assert!(HarnessKind::from_field_name("nope").is_none());
855 assert!(HarnessKind::Root.field_name().is_none());
856 }
857
858 #[test]
859 fn vm_harness_property_access_returns_sub_handle() {
860 let root = match Harness::real().into_vm_value() {
861 crate::value::VmValue::Harness(h) => h,
862 other => panic!("expected Harness variant, got {}", other.type_name()),
863 };
864 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
865 assert_eq!(stdio.kind(), HarnessKind::Stdio);
866 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
867 assert!(root.sub_handle("not_a_field").is_none());
868 }
869
870 #[test]
871 fn test_constructor_clock_advances_under_paused_clock_advance() {
872 let (harness, paused) = Harness::test();
873 let clock = harness.clock();
874 let start_wall = clock.clock().now_utc();
875 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
876 assert_eq!(clock.clock().monotonic_ms(), 0);
877
878 paused.advance(Duration::from_millis(1_500));
879 assert_eq!(clock.clock().monotonic_ms(), 1_500);
880 let after_wall = clock.clock().now_utc();
881 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
882 }
883
884 #[test]
885 fn with_paused_clock_pins_origin() {
886 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
887 let (harness, paused) = Harness::with_paused_clock(origin);
888 assert_eq!(harness.clock().clock().now_utc(), origin);
889 paused.advance(Duration::from_secs(60));
890 assert_eq!(
891 harness.clock().clock().now_utc() - origin,
892 time::Duration::seconds(60)
893 );
894 }
895
896 #[test]
897 fn null_harness_records_deny_events_for_every_sub_handle() {
898 let harness = Harness::null();
899 for source in [
900 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
901 r#"fn main(harness: Harness) { harness.clock.now_ms() }"#,
902 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
903 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
904 r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
905 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
906 r#"fn main(harness: Harness) { harness.system.cpu() }"#,
907 ] {
908 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
909 assert!(
910 error.contains("NullHarness denied"),
911 "unexpected deny error: {error}"
912 );
913 }
914
915 let events = harness.deny_events();
916 let observed: Vec<_> = events
917 .iter()
918 .map(|event| (event.sub_handle, event.method.as_str()))
919 .collect();
920 assert_eq!(
921 observed,
922 vec![
923 (HarnessKind::Stdio, "println"),
924 (HarnessKind::Clock, "now_ms"),
925 (HarnessKind::Fs, "read_text"),
926 (HarnessKind::Env, "get"),
927 (HarnessKind::Random, "gen_u64"),
928 (HarnessKind::Net, "get"),
929 (HarnessKind::System, "cpu"),
930 ]
931 );
932 assert_eq!(events[0].args, vec!["blocked"]);
933 assert_eq!(events[2].args, vec!["/x"]);
934 }
935
936 #[test]
937 fn mock_harness_replays_canned_responses_and_records_calls() {
938 let harness = Harness::mock()
939 .clock_at_unix_ms(1_700_000_000_000)
940 .env("KEY", "value")
941 .fs_read("/x", b"data".to_vec())
942 .random_u64(42)
943 .net_get("https://example.test", "body")
944 .build();
945
946 let output = run_harness_source(
947 r#"
948fn main(harness: Harness) {
949 harness.stdio.print("partial ")
950 harness.stdio.println("line")
951 __io_println(harness.clock.now_ms())
952 harness.clock.sleep_ms(250)
953 __io_println(harness.clock.now_ms())
954 __io_println(harness.clock.monotonic_ms())
955 __io_println(harness.env.get("KEY"))
956 __io_println(harness.fs.read_text("/x"))
957 __io_println(harness.fs.exists("/missing"))
958 __io_println(harness.random.gen_u64())
959 __io_println(harness.net.get("https://example.test"))
960}
961"#,
962 harness.clone(),
963 )
964 .expect("mock harness run succeeds");
965
966 assert_eq!(harness.captured_stdio(), "partial line\n");
967 assert_eq!(
968 output,
969 "1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\n"
970 );
971 let observed: Vec<_> = harness
972 .calls()
973 .into_iter()
974 .map(|call| (call.sub_handle, call.method))
975 .collect();
976 assert_eq!(
977 observed,
978 vec![
979 (HarnessKind::Stdio, "print".to_string()),
980 (HarnessKind::Stdio, "println".to_string()),
981 (HarnessKind::Clock, "now_ms".to_string()),
982 (HarnessKind::Clock, "sleep_ms".to_string()),
983 (HarnessKind::Clock, "now_ms".to_string()),
984 (HarnessKind::Clock, "monotonic_ms".to_string()),
985 (HarnessKind::Env, "get".to_string()),
986 (HarnessKind::Fs, "read_text".to_string()),
987 (HarnessKind::Fs, "exists".to_string()),
988 (HarnessKind::Random, "gen_u64".to_string()),
989 (HarnessKind::Net, "get".to_string()),
990 ]
991 );
992 }
993
994 #[test]
995 fn mock_harness_replays_random_values_fifo() {
996 let harness = Harness::mock()
997 .random_u64(7)
998 .random_u64(11)
999 .random_u64(u64::MAX)
1000 .build();
1001
1002 let output = run_harness_source(
1003 r#"
1004fn main(harness: Harness) {
1005 __io_println(harness.random.gen_u64())
1006 __io_println(harness.random.gen_u64())
1007 __io_println(harness.random.gen_u64())
1008}
1009"#,
1010 harness,
1011 )
1012 .expect("mock random succeeds");
1013
1014 assert_eq!(output, "7\n11\n9223372036854775807\n");
1015 }
1016
1017 #[test]
1018 fn mock_harness_reports_missing_canned_responses() {
1019 let cases = [
1020 (
1021 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1022 "MockHarness has no fs_read response for /missing",
1023 ),
1024 (
1025 r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
1026 "MockHarness has no random_u64 response",
1027 ),
1028 (
1029 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1030 "MockHarness has no net_get response for https://missing.test",
1031 ),
1032 ];
1033
1034 for (source, expected) in cases {
1035 let error = run_harness_source(source, Harness::mock().build())
1036 .expect_err("missing mock response fails");
1037 assert!(
1038 error.contains(expected),
1039 "expected `{expected}` in `{error}`"
1040 );
1041 }
1042 }
1043
1044 #[test]
1045 fn mock_harness_records_failed_calls() {
1046 let harness = Harness::mock().build();
1047 let error = run_harness_source(
1048 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1049 harness.clone(),
1050 )
1051 .expect_err("missing mock response fails");
1052
1053 assert!(error.contains("MockHarness has no net_get response"));
1054 assert_eq!(
1055 harness.calls(),
1056 vec![HarnessCall {
1057 sub_handle: HarnessKind::Net,
1058 method: "get".to_string(),
1059 args: vec!["https://missing.test".to_string()],
1060 }]
1061 );
1062 }
1063
1064 #[test]
1065 fn mock_harness_captures_stderr_separately_from_stdout() {
1066 let harness = Harness::mock().build();
1067 run_harness_source(
1068 r#"
1069fn main(harness: Harness) {
1070 harness.stdio.println("stdout line")
1071 harness.stdio.eprint("err ")
1072 harness.stdio.eprintln("trail")
1073}
1074"#,
1075 harness.clone(),
1076 )
1077 .expect("stderr capture run succeeds");
1078 assert_eq!(harness.captured_stdio(), "stdout line\n");
1079 assert_eq!(harness.captured_stderr(), "err trail\n");
1080 }
1081
1082 #[test]
1083 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1084 let harness = Harness::mock()
1085 .stdin_line("first")
1086 .stdin_line("second")
1087 .build();
1088 let output = run_harness_source(
1089 r#"
1090fn main(harness: Harness) {
1091 harness.stdio.println(harness.stdio.read_line())
1092 harness.stdio.println(harness.stdio.prompt("answer: "))
1093 let eof = harness.stdio.read_line({trim: false})
1094 harness.stdio.println(eof.status)
1095}
1096"#,
1097 harness.clone(),
1098 )
1099 .expect("stdin replay succeeds");
1100 assert_eq!(output, "");
1102 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1103 }
1104
1105 #[test]
1106 fn mock_harness_rejects_wrong_argument_types() {
1107 let error = run_harness_source(
1108 r#"fn main(harness: Harness) { harness.fs.read_text(1) }"#,
1109 Harness::mock().build(),
1110 )
1111 .expect_err("wrong argument type fails");
1112
1113 assert!(error.contains("HarnessFs.read_text expects string argument 1, got int"));
1114 }
1115
1116 #[test]
1117 fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1118 use crate::orchestration::{
1119 clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1120 };
1121 clear_execution_policy_stacks();
1122 let temp = tempfile::tempdir().unwrap();
1123 let policy = CapabilityPolicy {
1124 sandbox_profile: SandboxProfile::Worktree,
1125 workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1126 ..CapabilityPolicy::default()
1127 };
1128 push_execution_policy(policy);
1129 let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1130 let source = format!(
1131 r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1132 outside.to_string_lossy().replace('\\', "/"),
1133 );
1134 let error = run_harness_source(&source, Harness::real())
1135 .expect_err("write outside workspace_roots must reject");
1136 clear_execution_policy_stacks();
1137 assert!(
1138 error.contains("HARN-CAP-201"),
1139 "expected HARN-CAP-201 prefix, got: {error}"
1140 );
1141 assert!(
1142 error.contains("sandbox violation"),
1143 "deny should keep the underlying sandbox-rejection message, got: {error}"
1144 );
1145 }
1146
1147 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1148 let rt = tokio::runtime::Builder::new_current_thread()
1149 .enable_all()
1150 .build()
1151 .unwrap();
1152 rt.block_on(async move {
1153 let local = tokio::task::LocalSet::new();
1154 local
1155 .run_until(async move {
1156 let chunk = crate::compile_source(source)?;
1157 let mut vm = crate::Vm::new();
1158 crate::stdlib::register_vm_stdlib(&mut vm);
1159 vm.set_harness(harness);
1160 vm.execute(&chunk)
1161 .await
1162 .map_err(|error| error.to_string())?;
1163 Ok(vm.output().to_string())
1164 })
1165 .await
1166 })
1167 }
1168}