1use std::collections::{BTreeMap, VecDeque};
21use std::fmt;
22use std::rc::Rc;
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25
26use async_trait::async_trait;
27use harn_clock::{Clock, PausedClock, RealClock};
28use time::OffsetDateTime;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum HarnessKind {
36 Root,
37 Stdio,
38 Term,
39 Clock,
40 Fs,
41 Env,
42 Random,
43 Net,
44 Process,
45 Crypto,
46 System,
47 Llm,
48}
49
50impl HarnessKind {
51 pub const fn type_name(self) -> &'static str {
55 match self {
56 HarnessKind::Root => "Harness",
57 HarnessKind::Stdio => "HarnessStdio",
58 HarnessKind::Term => "HarnessTerm",
59 HarnessKind::Clock => "HarnessClock",
60 HarnessKind::Fs => "HarnessFs",
61 HarnessKind::Env => "HarnessEnv",
62 HarnessKind::Random => "HarnessRandom",
63 HarnessKind::Net => "HarnessNet",
64 HarnessKind::Process => "HarnessProcess",
65 HarnessKind::Crypto => "HarnessCrypto",
66 HarnessKind::System => "HarnessSystem",
67 HarnessKind::Llm => "HarnessLlm",
68 }
69 }
70
71 pub const fn field_name(self) -> Option<&'static str> {
74 match self {
75 HarnessKind::Root => None,
76 HarnessKind::Stdio => Some("stdio"),
77 HarnessKind::Term => Some("term"),
78 HarnessKind::Clock => Some("clock"),
79 HarnessKind::Fs => Some("fs"),
80 HarnessKind::Env => Some("env"),
81 HarnessKind::Random => Some("random"),
82 HarnessKind::Net => Some("net"),
83 HarnessKind::Process => Some("process"),
84 HarnessKind::Crypto => Some("crypto"),
85 HarnessKind::System => Some("system"),
86 HarnessKind::Llm => Some("llm"),
87 }
88 }
89
90 pub fn from_field_name(name: &str) -> Option<Self> {
92 match name {
93 "stdio" => Some(HarnessKind::Stdio),
94 "term" => Some(HarnessKind::Term),
95 "clock" => Some(HarnessKind::Clock),
96 "fs" => Some(HarnessKind::Fs),
97 "env" => Some(HarnessKind::Env),
98 "random" => Some(HarnessKind::Random),
99 "net" => Some(HarnessKind::Net),
100 "process" => Some(HarnessKind::Process),
101 "crypto" => Some(HarnessKind::Crypto),
102 "system" => Some(HarnessKind::System),
103 "llm" => Some(HarnessKind::Llm),
104 _ => None,
105 }
106 }
107
108 pub const SUB_HANDLES: &'static [HarnessKind] = &[
110 HarnessKind::Stdio,
111 HarnessKind::Term,
112 HarnessKind::Clock,
113 HarnessKind::Fs,
114 HarnessKind::Env,
115 HarnessKind::Random,
116 HarnessKind::Net,
117 HarnessKind::Process,
118 HarnessKind::Crypto,
119 HarnessKind::System,
120 HarnessKind::Llm,
121 ];
122
123 pub const ALL: &'static [HarnessKind] = &[
125 HarnessKind::Root,
126 HarnessKind::Stdio,
127 HarnessKind::Term,
128 HarnessKind::Clock,
129 HarnessKind::Fs,
130 HarnessKind::Env,
131 HarnessKind::Random,
132 HarnessKind::Net,
133 HarnessKind::Process,
134 HarnessKind::Crypto,
135 HarnessKind::System,
136 HarnessKind::Llm,
137 ];
138}
139
140#[derive(Debug)]
146pub struct HarnessInner {
147 clock: Arc<dyn Clock>,
148 mode: HarnessMode,
149 net_policy: Option<crate::harness_net::NetPolicy>,
154 quarantined: Mutex<bool>,
161}
162
163impl HarnessInner {
164 pub fn clock(&self) -> &Arc<dyn Clock> {
165 &self.clock
166 }
167
168 pub(crate) fn mode(&self) -> &HarnessMode {
169 &self.mode
170 }
171
172 pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
173 self.net_policy.as_ref()
174 }
175
176 pub(crate) fn mark_quarantined(&self) {
177 if let Ok(mut guard) = self.quarantined.lock() {
178 *guard = true;
179 }
180 }
181
182 pub fn is_quarantined(&self) -> bool {
183 self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
184 }
185}
186
187#[derive(Debug)]
188pub(crate) enum HarnessMode {
189 Real,
190 Null(NullHarnessState),
191 Mock(Arc<MockHarnessState>),
192}
193
194#[derive(Debug, Default)]
195pub(crate) struct NullHarnessState {
196 deny_events: Mutex<Vec<DenyEvent>>,
197}
198
199impl NullHarnessState {
200 pub(crate) fn record_deny(
201 &self,
202 sub_handle: HarnessKind,
203 method: &str,
204 args: &[crate::VmValue],
205 ) {
206 self.deny_events
207 .lock()
208 .expect("deny events poisoned")
209 .push(DenyEvent::new(
210 sub_handle,
211 method,
212 args.iter().map(crate::VmValue::display).collect(),
213 ));
214 }
215
216 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
217 self.deny_events
218 .lock()
219 .expect("deny events poisoned")
220 .clone()
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct DenyEvent {
226 pub sub_handle: HarnessKind,
227 pub method: String,
228 pub args: Vec<String>,
229}
230
231impl DenyEvent {
232 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
233 Self {
234 sub_handle,
235 method: method.to_string(),
236 args,
237 }
238 }
239}
240
241#[derive(Debug)]
242pub(crate) struct MockHarnessState {
243 calls: Mutex<Vec<HarnessCall>>,
244 clock: Arc<PausedClock>,
245 env: BTreeMap<String, String>,
246 fs_reads: BTreeMap<String, Vec<u8>>,
247 net_gets: BTreeMap<String, String>,
248 random_u64: Mutex<VecDeque<u64>>,
249 stdin_lines: Mutex<VecDeque<String>>,
250 stdio: Mutex<String>,
251 stderr: Mutex<String>,
252}
253
254impl MockHarnessState {
255 pub(crate) fn record_call(
256 &self,
257 sub_handle: HarnessKind,
258 method: &str,
259 args: &[crate::VmValue],
260 ) {
261 self.calls
262 .lock()
263 .expect("calls poisoned")
264 .push(HarnessCall::new(
265 sub_handle,
266 method,
267 args.iter().map(crate::VmValue::display).collect(),
268 ));
269 }
270
271 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
272 self.calls.lock().expect("calls poisoned").clone()
273 }
274
275 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
276 self.env.get(key).map(String::as_str)
277 }
278
279 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
280 self.fs_reads.get(path).map(Vec::as_slice)
281 }
282
283 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
284 self.net_gets.get(url).map(String::as_str)
285 }
286
287 pub(crate) fn next_random_u64(&self) -> Option<u64> {
288 let mut values = self.random_u64.lock().expect("random values poisoned");
289 values.pop_front()
290 }
291
292 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
293 self.clock.advance(duration);
294 }
295
296 pub(crate) fn push_stdio(&self, text: &str) {
297 self.stdio
298 .lock()
299 .expect("stdio buffer poisoned")
300 .push_str(text);
301 }
302
303 pub(crate) fn stdio(&self) -> String {
304 self.stdio.lock().expect("stdio buffer poisoned").clone()
305 }
306
307 pub(crate) fn push_stderr(&self, text: &str) {
308 self.stderr
309 .lock()
310 .expect("stderr buffer poisoned")
311 .push_str(text);
312 }
313
314 pub(crate) fn stderr(&self) -> String {
315 self.stderr.lock().expect("stderr buffer poisoned").clone()
316 }
317
318 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
319 self.stdin_lines
320 .lock()
321 .expect("stdin queue poisoned")
322 .pop_front()
323 }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct HarnessCall {
328 pub sub_handle: HarnessKind,
329 pub method: String,
330 pub args: Vec<String>,
331}
332
333impl HarnessCall {
334 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
335 Self {
336 sub_handle,
337 method: method.to_string(),
338 args,
339 }
340 }
341}
342
343#[derive(Debug)]
344pub struct MockHarnessBuilder {
345 clock: Arc<PausedClock>,
346 env: BTreeMap<String, String>,
347 fs_reads: BTreeMap<String, Vec<u8>>,
348 net_gets: BTreeMap<String, String>,
349 random_u64: Vec<u64>,
350 stdin_lines: Vec<String>,
351}
352
353impl MockHarnessBuilder {
354 fn new() -> Self {
355 Self {
356 clock: paused_clock_at_unix_ms(0),
357 env: BTreeMap::new(),
358 fs_reads: BTreeMap::new(),
359 net_gets: BTreeMap::new(),
360 random_u64: Vec::new(),
361 stdin_lines: Vec::new(),
362 }
363 }
364
365 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
366 self.clock = paused_clock_at_unix_ms(unix_ms);
367 self
368 }
369
370 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
371 self.clock = PausedClock::new(origin);
372 self
373 }
374
375 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
376 self.env.insert(key.into(), value.into());
377 self
378 }
379
380 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
381 self.fs_reads.insert(path.into(), data.into());
382 self
383 }
384
385 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
386 self.net_gets.insert(url.into(), body.into());
387 self
388 }
389
390 pub fn random_u64(mut self, value: u64) -> Self {
391 self.random_u64.push(value);
392 self
393 }
394
395 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
401 self.stdin_lines.push(line.into());
402 self
403 }
404
405 pub fn build(self) -> Harness {
406 let clock = self.clock;
407 Harness::with_mode(
408 clock.clone() as Arc<dyn Clock>,
409 HarnessMode::Mock(Arc::new(MockHarnessState {
410 calls: Mutex::new(Vec::new()),
411 clock,
412 env: self.env,
413 fs_reads: self.fs_reads,
414 net_gets: self.net_gets,
415 random_u64: Mutex::new(self.random_u64.into()),
416 stdin_lines: Mutex::new(self.stdin_lines.into()),
417 stdio: Mutex::new(String::new()),
418 stderr: Mutex::new(String::new()),
419 })),
420 )
421 }
422}
423
424#[derive(Debug, Clone)]
428pub struct Harness {
429 inner: Arc<HarnessInner>,
430}
431
432impl Harness {
433 pub fn real() -> Self {
444 Self::with_mode(
445 Arc::new(MockAwareClock::new(RealClock::new())),
446 HarnessMode::Real,
447 )
448 }
449
450 pub fn null() -> Self {
453 Self::with_mode(
454 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
455 HarnessMode::Null(NullHarnessState::default()),
456 )
457 }
458
459 pub fn mock() -> MockHarnessBuilder {
461 MockHarnessBuilder::new()
462 }
463
464 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
469 Self::with_mode(clock, HarnessMode::Real)
470 }
471
472 pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
477 Self { inner }
478 }
479
480 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
481 #[allow(clippy::arc_with_non_send_sync)]
489 let inner = Arc::new(HarnessInner {
490 clock,
491 mode,
492 net_policy: None,
493 quarantined: Mutex::new(false),
494 });
495 Self { inner }
496 }
497
498 pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
512 let clock = Arc::clone(&self.inner.clock);
513 let mode = match &self.inner.mode {
514 HarnessMode::Real => HarnessMode::Real,
515 HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
516 HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
517 };
518 #[allow(clippy::arc_with_non_send_sync)]
520 let inner = Arc::new(HarnessInner {
521 clock,
522 mode,
523 net_policy: Some(policy),
524 quarantined: Mutex::new(self.is_quarantined()),
525 });
526 Self { inner }
527 }
528
529 pub fn is_quarantined(&self) -> bool {
532 self.inner.is_quarantined()
533 }
534
535 pub fn deny_events(&self) -> Vec<DenyEvent> {
536 match self.inner.mode() {
537 HarnessMode::Null(state) => state.deny_events(),
538 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
539 }
540 }
541
542 pub fn calls(&self) -> Vec<HarnessCall> {
543 match self.inner.mode() {
544 HarnessMode::Mock(state) => state.calls(),
545 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
546 }
547 }
548
549 pub fn captured_stdio(&self) -> String {
550 match self.inner.mode() {
551 HarnessMode::Mock(state) => state.stdio(),
552 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
553 }
554 }
555
556 pub fn captured_stderr(&self) -> String {
557 match self.inner.mode() {
558 HarnessMode::Mock(state) => state.stderr(),
559 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
560 }
561 }
562
563 pub fn test() -> (Self, Arc<PausedClock>) {
575 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
576 }
577
578 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
582 let paused = PausedClock::new(origin);
583 let as_dyn: Arc<dyn Clock> = paused.clone();
584 (Self::with_clock(as_dyn), paused)
585 }
586
587 pub fn stdio(&self) -> HarnessStdio {
589 HarnessStdio {
590 inner: Arc::clone(&self.inner),
591 }
592 }
593
594 pub fn term(&self) -> HarnessTerm {
596 HarnessTerm {
597 inner: Arc::clone(&self.inner),
598 }
599 }
600
601 pub fn clock(&self) -> HarnessClock {
603 HarnessClock {
604 inner: Arc::clone(&self.inner),
605 }
606 }
607
608 pub fn fs(&self) -> HarnessFs {
610 HarnessFs {
611 inner: Arc::clone(&self.inner),
612 }
613 }
614
615 pub fn env(&self) -> HarnessEnv {
617 HarnessEnv {
618 inner: Arc::clone(&self.inner),
619 }
620 }
621
622 pub fn random(&self) -> HarnessRandom {
624 HarnessRandom {
625 inner: Arc::clone(&self.inner),
626 }
627 }
628
629 pub fn net(&self) -> HarnessNet {
631 HarnessNet {
632 inner: Arc::clone(&self.inner),
633 }
634 }
635
636 pub fn process(&self) -> HarnessProcess {
638 HarnessProcess {
639 inner: Arc::clone(&self.inner),
640 }
641 }
642
643 pub fn crypto(&self) -> HarnessCrypto {
645 HarnessCrypto {
646 inner: Arc::clone(&self.inner),
647 }
648 }
649
650 pub fn system(&self) -> HarnessSystem {
652 HarnessSystem {
653 inner: Arc::clone(&self.inner),
654 }
655 }
656
657 pub fn llm(&self) -> HarnessLlm {
659 HarnessLlm {
660 inner: Arc::clone(&self.inner),
661 }
662 }
663
664 pub fn into_vm_value(self) -> crate::value::VmValue {
666 crate::value::VmValue::harness(VmHarness {
667 inner: self.inner,
668 kind: HarnessKind::Root,
669 })
670 }
671}
672
673fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
674 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
675 let origin =
676 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
677 PausedClock::new(origin)
678}
679
680pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
681 crate::VmValue::String(Rc::from(value.into()))
682}
683
684impl Default for Harness {
685 fn default() -> Self {
686 Self::real()
687 }
688}
689
690#[derive(Debug, Clone)]
693pub struct HarnessStdio {
694 inner: Arc<HarnessInner>,
695}
696
697#[derive(Debug, Clone)]
699pub struct HarnessTerm {
700 inner: Arc<HarnessInner>,
701}
702
703#[derive(Debug, Clone)]
705pub struct HarnessClock {
706 inner: Arc<HarnessInner>,
707}
708
709impl HarnessClock {
710 pub fn clock(&self) -> &Arc<dyn Clock> {
711 self.inner.clock()
712 }
713}
714
715#[derive(Debug, Clone)]
718pub struct HarnessFs {
719 inner: Arc<HarnessInner>,
720}
721
722#[derive(Debug, Clone)]
724pub struct HarnessEnv {
725 inner: Arc<HarnessInner>,
726}
727
728#[derive(Debug, Clone)]
730pub struct HarnessRandom {
731 inner: Arc<HarnessInner>,
732}
733
734#[derive(Debug, Clone)]
736pub struct HarnessNet {
737 inner: Arc<HarnessInner>,
738}
739
740#[derive(Debug, Clone)]
742pub struct HarnessProcess {
743 inner: Arc<HarnessInner>,
744}
745
746#[derive(Debug, Clone)]
748pub struct HarnessCrypto {
749 inner: Arc<HarnessInner>,
750}
751
752#[derive(Debug, Clone)]
758pub struct HarnessSystem {
759 inner: Arc<HarnessInner>,
760}
761
762#[derive(Debug, Clone)]
764pub struct HarnessLlm {
765 inner: Arc<HarnessInner>,
766}
767
768macro_rules! sub_handle_inner {
769 ($($ty:ty),* $(,)?) => {
770 $(
771 impl $ty {
772 #[allow(dead_code)]
773 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
774 &self.inner
775 }
776 }
777 )*
778 };
779}
780sub_handle_inner!(
781 HarnessStdio,
782 HarnessTerm,
783 HarnessFs,
784 HarnessEnv,
785 HarnessRandom,
786 HarnessNet,
787 HarnessProcess,
788 HarnessCrypto,
789 HarnessSystem,
790 HarnessLlm,
791);
792
793impl HarnessClock {
794 #[allow(dead_code)]
795 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
796 &self.inner
797 }
798}
799
800#[derive(Clone)]
805pub struct VmHarness {
806 inner: Arc<HarnessInner>,
807 kind: HarnessKind,
808}
809
810impl VmHarness {
811 pub fn kind(&self) -> HarnessKind {
812 self.kind
813 }
814
815 pub fn type_name(&self) -> &'static str {
816 self.kind.type_name()
817 }
818
819 pub fn inner(&self) -> &Arc<HarnessInner> {
820 &self.inner
821 }
822
823 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
827 if self.kind != HarnessKind::Root {
828 return None;
829 }
830 let kind = HarnessKind::from_field_name(field)?;
831 Some(VmHarness {
832 inner: Arc::clone(&self.inner),
833 kind,
834 })
835 }
836}
837
838impl fmt::Debug for VmHarness {
839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840 f.debug_struct("VmHarness")
841 .field("kind", &self.kind)
842 .finish_non_exhaustive()
843 }
844}
845
846#[derive(Debug)]
852pub struct MockAwareClock<C: Clock + 'static> {
853 inner: C,
854}
855
856impl<C: Clock + 'static> MockAwareClock<C> {
857 pub fn new(inner: C) -> Self {
858 Self { inner }
859 }
860}
861
862#[async_trait]
863impl<C: Clock + 'static> Clock for MockAwareClock<C> {
864 fn now_utc(&self) -> OffsetDateTime {
865 if let Some(mock) = crate::clock_mock::active_mock_clock() {
866 return mock.now_utc();
867 }
868 self.inner.now_utc()
869 }
870
871 fn monotonic_ms(&self) -> i64 {
872 if let Some(mock) = crate::clock_mock::active_mock_clock() {
873 return mock.monotonic_ms();
874 }
875 self.inner.monotonic_ms()
876 }
877
878 async fn sleep(&self, duration: Duration) {
879 if duration.is_zero() {
880 return;
881 }
882 if let Some(mock) = crate::clock_mock::active_mock_clock() {
883 mock.advance_std_sync(duration);
889 return;
890 }
891 self.inner.sleep(duration).await;
892 }
893
894 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
895 if let Some(mock) = crate::clock_mock::active_mock_clock() {
896 let now = mock.now_utc();
897 if deadline > now {
898 if let Ok(delta) = Duration::try_from(deadline - now) {
899 mock.advance_std_sync(delta);
900 }
901 }
902 return;
903 }
904 self.inner.sleep_until_utc(deadline).await;
905 }
906}
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 #[test]
913 fn real_constructs_without_panic() {
914 let _harness = Harness::real();
915 }
916
917 #[test]
918 fn sub_handles_share_inner_state() {
919 let harness = Harness::real();
920 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
921 let clock_inner = Arc::as_ptr(harness.clock().inner());
922 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
923 }
924
925 #[test]
926 fn kinds_round_trip_through_field_names() {
927 for kind in HarnessKind::SUB_HANDLES {
928 let field = kind.field_name().unwrap();
929 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
930 }
931 assert!(HarnessKind::from_field_name("nope").is_none());
932 assert!(HarnessKind::Root.field_name().is_none());
933 }
934
935 #[test]
936 fn vm_harness_property_access_returns_sub_handle() {
937 let root = match Harness::real().into_vm_value() {
938 crate::value::VmValue::Harness(h) => h,
939 other => panic!("expected Harness variant, got {}", other.type_name()),
940 };
941 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
942 assert_eq!(stdio.kind(), HarnessKind::Stdio);
943 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
944 assert!(root.sub_handle("not_a_field").is_none());
945 }
946
947 #[test]
948 fn test_constructor_clock_advances_under_paused_clock_advance() {
949 let (harness, paused) = Harness::test();
950 let clock = harness.clock();
951 let start_wall = clock.clock().now_utc();
952 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
953 assert_eq!(clock.clock().monotonic_ms(), 0);
954
955 paused.advance(Duration::from_millis(1_500));
956 assert_eq!(clock.clock().monotonic_ms(), 1_500);
957 let after_wall = clock.clock().now_utc();
958 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
959 }
960
961 #[test]
962 fn with_paused_clock_pins_origin() {
963 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
964 let (harness, paused) = Harness::with_paused_clock(origin);
965 assert_eq!(harness.clock().clock().now_utc(), origin);
966 paused.advance(Duration::from_secs(60));
967 assert_eq!(
968 harness.clock().clock().now_utc() - origin,
969 time::Duration::seconds(60)
970 );
971 }
972
973 #[test]
974 fn null_harness_records_deny_events_for_every_sub_handle() {
975 let harness = Harness::null();
976 for source in [
977 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
978 r#"fn main(harness: Harness) { harness.term.width() }"#,
979 r#"fn main(harness: Harness) { harness.clock.now_ms() }"#,
980 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
981 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
982 r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
983 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
984 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
985 r#"fn main(harness: Harness) { harness.crypto.sha256("") }"#,
986 r#"fn main(harness: Harness) { harness.system.cpu() }"#,
987 r#"fn main(harness: Harness) { harness.llm.catalog() }"#,
988 ] {
989 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
990 assert!(
991 error.contains("NullHarness denied"),
992 "unexpected deny error: {error}"
993 );
994 }
995
996 let events = harness.deny_events();
997 let observed: Vec<_> = events
998 .iter()
999 .map(|event| (event.sub_handle, event.method.as_str()))
1000 .collect();
1001 assert_eq!(
1002 observed,
1003 vec![
1004 (HarnessKind::Stdio, "println"),
1005 (HarnessKind::Term, "width"),
1006 (HarnessKind::Clock, "now_ms"),
1007 (HarnessKind::Fs, "read_text"),
1008 (HarnessKind::Env, "get"),
1009 (HarnessKind::Random, "gen_u64"),
1010 (HarnessKind::Net, "get"),
1011 (HarnessKind::Process, "spawn_captured"),
1012 (HarnessKind::Crypto, "sha256"),
1013 (HarnessKind::System, "cpu"),
1014 (HarnessKind::Llm, "catalog"),
1015 ]
1016 );
1017 assert_eq!(events[0].args, vec!["blocked"]);
1018 assert_eq!(events[3].args, vec!["/x"]);
1019 }
1020
1021 #[test]
1022 fn mock_harness_replays_canned_responses_and_records_calls() {
1023 let harness = Harness::mock()
1024 .clock_at_unix_ms(1_700_000_000_000)
1025 .env("KEY", "value")
1026 .fs_read("/x", b"data".to_vec())
1027 .random_u64(42)
1028 .net_get("https://example.test", "body")
1029 .build();
1030
1031 let output = run_harness_source(
1032 r#"
1033fn main(harness: Harness) {
1034 harness.stdio.print("partial ")
1035 harness.stdio.println("line")
1036 __io_println(harness.term.width())
1037 __io_println(harness.term.height())
1038 __io_println(harness.clock.now_ms())
1039 harness.clock.sleep_ms(250)
1040 __io_println(harness.clock.now_ms())
1041 __io_println(harness.clock.monotonic_ms())
1042 __io_println(harness.env.get("KEY"))
1043 __io_println(harness.fs.read_text("/x"))
1044 __io_println(harness.fs.exists("/missing"))
1045 __io_println(harness.random.gen_u64())
1046 __io_println(harness.net.get("https://example.test"))
1047 __io_println(harness.crypto.sha256(""))
1048 __io_println(len(harness.llm.catalog()) > 0)
1049}
1050"#,
1051 harness.clone(),
1052 )
1053 .expect("mock harness run succeeds");
1054
1055 assert_eq!(harness.captured_stdio(), "partial line\n");
1056 assert_eq!(
1057 output,
1058 "80\n24\n1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ntrue\n"
1059 );
1060 let observed: Vec<_> = harness
1061 .calls()
1062 .into_iter()
1063 .map(|call| (call.sub_handle, call.method))
1064 .collect();
1065 assert_eq!(
1066 observed,
1067 vec![
1068 (HarnessKind::Stdio, "print".to_string()),
1069 (HarnessKind::Stdio, "println".to_string()),
1070 (HarnessKind::Term, "width".to_string()),
1071 (HarnessKind::Term, "height".to_string()),
1072 (HarnessKind::Clock, "now_ms".to_string()),
1073 (HarnessKind::Clock, "sleep_ms".to_string()),
1074 (HarnessKind::Clock, "now_ms".to_string()),
1075 (HarnessKind::Clock, "monotonic_ms".to_string()),
1076 (HarnessKind::Env, "get".to_string()),
1077 (HarnessKind::Fs, "read_text".to_string()),
1078 (HarnessKind::Fs, "exists".to_string()),
1079 (HarnessKind::Random, "gen_u64".to_string()),
1080 (HarnessKind::Net, "get".to_string()),
1081 (HarnessKind::Crypto, "sha256".to_string()),
1082 (HarnessKind::Llm, "catalog".to_string()),
1083 ]
1084 );
1085 }
1086
1087 #[test]
1088 fn mock_harness_replays_random_values_fifo() {
1089 let harness = Harness::mock()
1090 .random_u64(7)
1091 .random_u64(11)
1092 .random_u64(u64::MAX)
1093 .build();
1094
1095 let output = run_harness_source(
1096 r#"
1097fn main(harness: Harness) {
1098 __io_println(harness.random.gen_u64())
1099 __io_println(harness.random.gen_u64())
1100 __io_println(harness.random.gen_u64())
1101}
1102"#,
1103 harness,
1104 )
1105 .expect("mock random succeeds");
1106
1107 assert_eq!(output, "7\n11\n9223372036854775807\n");
1108 }
1109
1110 #[test]
1111 fn mock_harness_reports_missing_canned_responses() {
1112 let cases = [
1113 (
1114 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1115 "MockHarness has no fs_read response for /missing",
1116 ),
1117 (
1118 r#"fn main(harness: Harness) { harness.random.gen_u64() }"#,
1119 "MockHarness has no random_u64 response",
1120 ),
1121 (
1122 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1123 "MockHarness has no net_get response for https://missing.test",
1124 ),
1125 (
1126 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1127 "MockHarness has no process spawn response",
1128 ),
1129 ];
1130
1131 for (source, expected) in cases {
1132 let error = run_harness_source(source, Harness::mock().build())
1133 .expect_err("missing mock response fails");
1134 assert!(
1135 error.contains(expected),
1136 "expected `{expected}` in `{error}`"
1137 );
1138 }
1139 }
1140
1141 #[test]
1142 fn mock_harness_records_failed_calls() {
1143 let harness = Harness::mock().build();
1144 let error = run_harness_source(
1145 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1146 harness.clone(),
1147 )
1148 .expect_err("missing mock response fails");
1149
1150 assert!(error.contains("MockHarness has no net_get response"));
1151 assert_eq!(
1152 harness.calls(),
1153 vec![HarnessCall {
1154 sub_handle: HarnessKind::Net,
1155 method: "get".to_string(),
1156 args: vec!["https://missing.test".to_string()],
1157 }]
1158 );
1159 }
1160
1161 #[test]
1162 fn mock_harness_captures_stderr_separately_from_stdout() {
1163 let harness = Harness::mock().build();
1164 run_harness_source(
1165 r#"
1166fn main(harness: Harness) {
1167 harness.stdio.println("stdout line")
1168 harness.stdio.eprint("err ")
1169 harness.stdio.eprintln("trail")
1170}
1171"#,
1172 harness.clone(),
1173 )
1174 .expect("stderr capture run succeeds");
1175 assert_eq!(harness.captured_stdio(), "stdout line\n");
1176 assert_eq!(harness.captured_stderr(), "err trail\n");
1177 }
1178
1179 #[test]
1180 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1181 let harness = Harness::mock()
1182 .stdin_line("first")
1183 .stdin_line("second")
1184 .build();
1185 let output = run_harness_source(
1186 r#"
1187fn main(harness: Harness) {
1188 harness.stdio.println(harness.stdio.read_line())
1189 harness.stdio.println(harness.stdio.prompt("answer: "))
1190 let eof = harness.stdio.read_line({trim: false})
1191 harness.stdio.println(eof.status)
1192}
1193"#,
1194 harness.clone(),
1195 )
1196 .expect("stdin replay succeeds");
1197 assert_eq!(output, "");
1199 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1200 }
1201
1202 #[test]
1203 fn mock_harness_replays_password_input_without_stdout_echo() {
1204 let harness = Harness::mock().stdin_line("secret").build();
1205 let output = run_harness_source(
1206 r#"
1207fn main(harness: Harness) {
1208 __io_println(harness.term.read_password("password: "))
1209}
1210"#,
1211 harness.clone(),
1212 )
1213 .expect("stdin replay succeeds");
1214
1215 assert_eq!(output, "secret\n");
1216 assert_eq!(harness.captured_stdio(), "");
1217 assert_eq!(harness.captured_stderr(), "password: ");
1218 assert_eq!(
1219 harness.calls(),
1220 vec![HarnessCall {
1221 sub_handle: HarnessKind::Term,
1222 method: "read_password".to_string(),
1223 args: vec!["password: ".to_string()],
1224 }]
1225 );
1226 }
1227
1228 #[test]
1229 fn mock_harness_rejects_wrong_argument_types() {
1230 let error = run_harness_source(
1231 r#"fn main(harness: Harness) { harness.fs.read_text(1) }"#,
1232 Harness::mock().build(),
1233 )
1234 .expect_err("wrong argument type fails");
1235
1236 assert!(error.contains("HarnessFs.read_text expects string argument 1, got int"));
1237 }
1238
1239 #[test]
1240 fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1241 use crate::orchestration::{
1242 clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1243 };
1244 clear_execution_policy_stacks();
1245 let temp = tempfile::tempdir().unwrap();
1246 let policy = CapabilityPolicy {
1247 sandbox_profile: SandboxProfile::Worktree,
1248 workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1249 ..CapabilityPolicy::default()
1250 };
1251 push_execution_policy(policy);
1252 let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1253 let source = format!(
1254 r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1255 outside.to_string_lossy().replace('\\', "/"),
1256 );
1257 let error = run_harness_source(&source, Harness::real())
1258 .expect_err("write outside workspace_roots must reject");
1259 clear_execution_policy_stacks();
1260 assert!(
1261 error.contains("HARN-CAP-201"),
1262 "expected HARN-CAP-201 prefix, got: {error}"
1263 );
1264 assert!(
1265 error.contains("sandbox violation"),
1266 "deny should keep the underlying sandbox-rejection message, got: {error}"
1267 );
1268 }
1269
1270 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1271 let rt = tokio::runtime::Builder::new_current_thread()
1272 .enable_all()
1273 .build()
1274 .unwrap();
1275 rt.block_on(async move {
1276 let local = tokio::task::LocalSet::new();
1277 local
1278 .run_until(async move {
1279 let chunk = crate::compile_source(source)?;
1280 let mut vm = crate::Vm::new();
1281 crate::stdlib::register_vm_stdlib(&mut vm);
1282 vm.set_harness(harness);
1283 vm.execute(&chunk)
1284 .await
1285 .map_err(|error| error.to_string())?;
1286 Ok(vm.output().to_string())
1287 })
1288 .await
1289 })
1290 }
1291}