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 Tenant,
52 Obs,
58}
59
60impl HarnessKind {
61 pub const fn type_name(self) -> &'static str {
65 match self {
66 HarnessKind::Root => "Harness",
67 HarnessKind::Stdio => "HarnessStdio",
68 HarnessKind::Term => "HarnessTerm",
69 HarnessKind::Clock => "HarnessClock",
70 HarnessKind::Fs => "HarnessFs",
71 HarnessKind::Env => "HarnessEnv",
72 HarnessKind::Random => "HarnessRandom",
73 HarnessKind::Net => "HarnessNet",
74 HarnessKind::Process => "HarnessProcess",
75 HarnessKind::Crypto => "HarnessCrypto",
76 HarnessKind::System => "HarnessSystem",
77 HarnessKind::Llm => "HarnessLlm",
78 HarnessKind::Tenant => "HarnessTenant",
79 HarnessKind::Obs => "HarnessObs",
80 }
81 }
82
83 pub const fn field_name(self) -> Option<&'static str> {
86 match self {
87 HarnessKind::Root => None,
88 HarnessKind::Stdio => Some("stdio"),
89 HarnessKind::Term => Some("term"),
90 HarnessKind::Clock => Some("clock"),
91 HarnessKind::Fs => Some("fs"),
92 HarnessKind::Env => Some("env"),
93 HarnessKind::Random => Some("random"),
94 HarnessKind::Net => Some("net"),
95 HarnessKind::Process => Some("process"),
96 HarnessKind::Crypto => Some("crypto"),
97 HarnessKind::System => Some("system"),
98 HarnessKind::Llm => Some("llm"),
99 HarnessKind::Tenant => Some("tenant"),
100 HarnessKind::Obs => Some("obs"),
101 }
102 }
103
104 pub fn from_field_name(name: &str) -> Option<Self> {
106 match name {
107 "stdio" => Some(HarnessKind::Stdio),
108 "term" => Some(HarnessKind::Term),
109 "clock" => Some(HarnessKind::Clock),
110 "fs" => Some(HarnessKind::Fs),
111 "env" => Some(HarnessKind::Env),
112 "random" => Some(HarnessKind::Random),
113 "net" => Some(HarnessKind::Net),
114 "process" => Some(HarnessKind::Process),
115 "crypto" => Some(HarnessKind::Crypto),
116 "system" => Some(HarnessKind::System),
117 "llm" => Some(HarnessKind::Llm),
118 "tenant" => Some(HarnessKind::Tenant),
119 "obs" => Some(HarnessKind::Obs),
120 _ => None,
121 }
122 }
123
124 pub const SUB_HANDLES: &'static [HarnessKind] = &[
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 HarnessKind::Tenant,
138 HarnessKind::Obs,
139 ];
140
141 pub const ALL: &'static [HarnessKind] = &[
143 HarnessKind::Root,
144 HarnessKind::Stdio,
145 HarnessKind::Term,
146 HarnessKind::Clock,
147 HarnessKind::Fs,
148 HarnessKind::Env,
149 HarnessKind::Random,
150 HarnessKind::Net,
151 HarnessKind::Process,
152 HarnessKind::Crypto,
153 HarnessKind::System,
154 HarnessKind::Llm,
155 HarnessKind::Tenant,
156 HarnessKind::Obs,
157 ];
158}
159
160#[derive(Debug)]
166pub struct HarnessInner {
167 clock: Arc<dyn Clock>,
168 mode: HarnessMode,
169 net_policy: Option<crate::harness_net::NetPolicy>,
174 quarantined: Mutex<bool>,
181}
182
183impl HarnessInner {
184 pub fn clock(&self) -> &Arc<dyn Clock> {
185 &self.clock
186 }
187
188 pub(crate) fn mode(&self) -> &HarnessMode {
189 &self.mode
190 }
191
192 pub fn net_policy(&self) -> Option<&crate::harness_net::NetPolicy> {
193 self.net_policy.as_ref()
194 }
195
196 pub(crate) fn mark_quarantined(&self) {
197 if let Ok(mut guard) = self.quarantined.lock() {
198 *guard = true;
199 }
200 }
201
202 pub fn is_quarantined(&self) -> bool {
203 self.quarantined.lock().map(|guard| *guard).unwrap_or(false)
204 }
205}
206
207#[derive(Debug)]
208pub(crate) enum HarnessMode {
209 Real,
210 Null(NullHarnessState),
211 Mock(Arc<MockHarnessState>),
212}
213
214#[derive(Debug, Default)]
215pub(crate) struct NullHarnessState {
216 deny_events: Mutex<Vec<DenyEvent>>,
217}
218
219impl NullHarnessState {
220 pub(crate) fn record_deny(
221 &self,
222 sub_handle: HarnessKind,
223 method: &str,
224 args: &[crate::VmValue],
225 ) {
226 self.deny_events
227 .lock()
228 .expect("deny events poisoned")
229 .push(DenyEvent::new(
230 sub_handle,
231 method,
232 args.iter().map(crate::VmValue::display).collect(),
233 ));
234 }
235
236 pub(crate) fn deny_events(&self) -> Vec<DenyEvent> {
237 self.deny_events
238 .lock()
239 .expect("deny events poisoned")
240 .clone()
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct DenyEvent {
246 pub sub_handle: HarnessKind,
247 pub method: String,
248 pub args: Vec<String>,
249}
250
251impl DenyEvent {
252 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
253 Self {
254 sub_handle,
255 method: method.to_string(),
256 args,
257 }
258 }
259}
260
261#[derive(Debug)]
262pub(crate) struct MockHarnessState {
263 calls: Mutex<Vec<HarnessCall>>,
264 clock: Arc<PausedClock>,
265 env: BTreeMap<String, String>,
266 fs_reads: BTreeMap<String, Vec<u8>>,
267 net_gets: BTreeMap<String, String>,
268 random_u64: Mutex<VecDeque<u64>>,
269 stdin_lines: Mutex<VecDeque<String>>,
270 stdio: Mutex<String>,
271 stderr: Mutex<String>,
272}
273
274impl MockHarnessState {
275 pub(crate) fn record_call(
276 &self,
277 sub_handle: HarnessKind,
278 method: &str,
279 args: &[crate::VmValue],
280 ) {
281 self.calls
282 .lock()
283 .expect("calls poisoned")
284 .push(HarnessCall::new(
285 sub_handle,
286 method,
287 args.iter().map(crate::VmValue::display).collect(),
288 ));
289 }
290
291 pub(crate) fn calls(&self) -> Vec<HarnessCall> {
292 self.calls.lock().expect("calls poisoned").clone()
293 }
294
295 pub(crate) fn env_get(&self, key: &str) -> Option<&str> {
296 self.env.get(key).map(String::as_str)
297 }
298
299 pub(crate) fn fs_read(&self, path: &str) -> Option<&[u8]> {
300 self.fs_reads.get(path).map(Vec::as_slice)
301 }
302
303 pub(crate) fn net_get(&self, url: &str) -> Option<&str> {
304 self.net_gets.get(url).map(String::as_str)
305 }
306
307 pub(crate) fn next_random_u64(&self) -> Option<u64> {
308 let mut values = self.random_u64.lock().expect("random values poisoned");
309 values.pop_front()
310 }
311
312 pub(crate) fn advance_clock(&self, duration: std::time::Duration) {
313 self.clock.advance(duration);
314 }
315
316 pub(crate) fn push_stdio(&self, text: &str) {
317 self.stdio
318 .lock()
319 .expect("stdio buffer poisoned")
320 .push_str(text);
321 }
322
323 pub(crate) fn stdio(&self) -> String {
324 self.stdio.lock().expect("stdio buffer poisoned").clone()
325 }
326
327 pub(crate) fn push_stderr(&self, text: &str) {
328 self.stderr
329 .lock()
330 .expect("stderr buffer poisoned")
331 .push_str(text);
332 }
333
334 pub(crate) fn stderr(&self) -> String {
335 self.stderr.lock().expect("stderr buffer poisoned").clone()
336 }
337
338 pub(crate) fn pop_stdin_line(&self) -> Option<String> {
339 self.stdin_lines
340 .lock()
341 .expect("stdin queue poisoned")
342 .pop_front()
343 }
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub struct HarnessCall {
348 pub sub_handle: HarnessKind,
349 pub method: String,
350 pub args: Vec<String>,
351}
352
353impl HarnessCall {
354 fn new(sub_handle: HarnessKind, method: &str, args: Vec<String>) -> Self {
355 Self {
356 sub_handle,
357 method: method.to_string(),
358 args,
359 }
360 }
361}
362
363#[derive(Debug)]
364pub struct MockHarnessBuilder {
365 clock: Arc<PausedClock>,
366 env: BTreeMap<String, String>,
367 fs_reads: BTreeMap<String, Vec<u8>>,
368 net_gets: BTreeMap<String, String>,
369 random_u64: Vec<u64>,
370 stdin_lines: Vec<String>,
371}
372
373impl MockHarnessBuilder {
374 fn new() -> Self {
375 Self {
376 clock: paused_clock_at_unix_ms(0),
377 env: BTreeMap::new(),
378 fs_reads: BTreeMap::new(),
379 net_gets: BTreeMap::new(),
380 random_u64: Vec::new(),
381 stdin_lines: Vec::new(),
382 }
383 }
384
385 pub fn clock_at_unix_ms(mut self, unix_ms: i64) -> Self {
386 self.clock = paused_clock_at_unix_ms(unix_ms);
387 self
388 }
389
390 pub fn clock_at(mut self, origin: OffsetDateTime) -> Self {
391 self.clock = PausedClock::new(origin);
392 self
393 }
394
395 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
396 self.env.insert(key.into(), value.into());
397 self
398 }
399
400 pub fn fs_read(mut self, path: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
401 self.fs_reads.insert(path.into(), data.into());
402 self
403 }
404
405 pub fn net_get(mut self, url: impl Into<String>, body: impl Into<String>) -> Self {
406 self.net_gets.insert(url.into(), body.into());
407 self
408 }
409
410 pub fn random_u64(mut self, value: u64) -> Self {
411 self.random_u64.push(value);
412 self
413 }
414
415 pub fn stdin_line(mut self, line: impl Into<String>) -> Self {
421 self.stdin_lines.push(line.into());
422 self
423 }
424
425 pub fn build(self) -> Harness {
426 let clock = self.clock;
427 Harness::with_mode(
428 clock.clone() as Arc<dyn Clock>,
429 HarnessMode::Mock(Arc::new(MockHarnessState {
430 calls: Mutex::new(Vec::new()),
431 clock,
432 env: self.env,
433 fs_reads: self.fs_reads,
434 net_gets: self.net_gets,
435 random_u64: Mutex::new(self.random_u64.into()),
436 stdin_lines: Mutex::new(self.stdin_lines.into()),
437 stdio: Mutex::new(String::new()),
438 stderr: Mutex::new(String::new()),
439 })),
440 )
441 }
442}
443
444#[derive(Debug, Clone)]
448pub struct Harness {
449 inner: Arc<HarnessInner>,
450}
451
452impl Harness {
453 pub fn real() -> Self {
464 Self::with_mode(
465 Arc::new(MockAwareClock::new(RealClock::new())),
466 HarnessMode::Real,
467 )
468 }
469
470 pub fn null() -> Self {
473 Self::with_mode(
474 paused_clock_at_unix_ms(0) as Arc<dyn Clock>,
475 HarnessMode::Null(NullHarnessState::default()),
476 )
477 }
478
479 pub fn mock() -> MockHarnessBuilder {
481 MockHarnessBuilder::new()
482 }
483
484 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
489 Self::with_mode(clock, HarnessMode::Real)
490 }
491
492 pub fn from_inner(inner: Arc<HarnessInner>) -> Self {
497 Self { inner }
498 }
499
500 fn with_mode(clock: Arc<dyn Clock>, mode: HarnessMode) -> Self {
501 #[allow(clippy::arc_with_non_send_sync)]
509 let inner = Arc::new(HarnessInner {
510 clock,
511 mode,
512 net_policy: None,
513 quarantined: Mutex::new(false),
514 });
515 Self { inner }
516 }
517
518 pub fn with_net_policy(&self, policy: crate::harness_net::NetPolicy) -> Self {
532 let clock = Arc::clone(&self.inner.clock);
533 let mode = match &self.inner.mode {
534 HarnessMode::Real => HarnessMode::Real,
535 HarnessMode::Null(_) => HarnessMode::Null(NullHarnessState::default()),
536 HarnessMode::Mock(state) => HarnessMode::Mock(Arc::clone(state)),
537 };
538 #[allow(clippy::arc_with_non_send_sync)]
540 let inner = Arc::new(HarnessInner {
541 clock,
542 mode,
543 net_policy: Some(policy),
544 quarantined: Mutex::new(self.is_quarantined()),
545 });
546 Self { inner }
547 }
548
549 pub fn is_quarantined(&self) -> bool {
552 self.inner.is_quarantined()
553 }
554
555 pub fn deny_events(&self) -> Vec<DenyEvent> {
556 match self.inner.mode() {
557 HarnessMode::Null(state) => state.deny_events(),
558 HarnessMode::Real | HarnessMode::Mock(_) => Vec::new(),
559 }
560 }
561
562 pub fn calls(&self) -> Vec<HarnessCall> {
563 match self.inner.mode() {
564 HarnessMode::Mock(state) => state.calls(),
565 HarnessMode::Real | HarnessMode::Null(_) => Vec::new(),
566 }
567 }
568
569 pub fn captured_stdio(&self) -> String {
570 match self.inner.mode() {
571 HarnessMode::Mock(state) => state.stdio(),
572 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
573 }
574 }
575
576 pub fn captured_stderr(&self) -> String {
577 match self.inner.mode() {
578 HarnessMode::Mock(state) => state.stderr(),
579 HarnessMode::Real | HarnessMode::Null(_) => String::new(),
580 }
581 }
582
583 pub fn test() -> (Self, Arc<PausedClock>) {
595 Self::with_paused_clock(OffsetDateTime::UNIX_EPOCH)
596 }
597
598 pub fn with_paused_clock(origin: OffsetDateTime) -> (Self, Arc<PausedClock>) {
602 let paused = PausedClock::new(origin);
603 let as_dyn: Arc<dyn Clock> = paused.clone();
604 (Self::with_clock(as_dyn), paused)
605 }
606
607 pub fn stdio(&self) -> HarnessStdio {
609 HarnessStdio {
610 inner: Arc::clone(&self.inner),
611 }
612 }
613
614 pub fn term(&self) -> HarnessTerm {
616 HarnessTerm {
617 inner: Arc::clone(&self.inner),
618 }
619 }
620
621 pub fn clock(&self) -> HarnessClock {
623 HarnessClock {
624 inner: Arc::clone(&self.inner),
625 }
626 }
627
628 pub fn fs(&self) -> HarnessFs {
630 HarnessFs {
631 inner: Arc::clone(&self.inner),
632 }
633 }
634
635 pub fn env(&self) -> HarnessEnv {
637 HarnessEnv {
638 inner: Arc::clone(&self.inner),
639 }
640 }
641
642 pub fn random(&self) -> HarnessRandom {
644 HarnessRandom {
645 inner: Arc::clone(&self.inner),
646 }
647 }
648
649 pub fn net(&self) -> HarnessNet {
651 HarnessNet {
652 inner: Arc::clone(&self.inner),
653 }
654 }
655
656 pub fn process(&self) -> HarnessProcess {
658 HarnessProcess {
659 inner: Arc::clone(&self.inner),
660 }
661 }
662
663 pub fn crypto(&self) -> HarnessCrypto {
665 HarnessCrypto {
666 inner: Arc::clone(&self.inner),
667 }
668 }
669
670 pub fn system(&self) -> HarnessSystem {
672 HarnessSystem {
673 inner: Arc::clone(&self.inner),
674 }
675 }
676
677 pub fn llm(&self) -> HarnessLlm {
679 HarnessLlm {
680 inner: Arc::clone(&self.inner),
681 }
682 }
683
684 pub fn tenant(&self) -> HarnessTenant {
686 HarnessTenant {
687 inner: Arc::clone(&self.inner),
688 }
689 }
690
691 pub fn obs(&self) -> HarnessObs {
693 HarnessObs {
694 inner: Arc::clone(&self.inner),
695 }
696 }
697
698 pub fn into_vm_value(self) -> crate::value::VmValue {
700 crate::value::VmValue::harness(VmHarness {
701 inner: self.inner,
702 kind: HarnessKind::Root,
703 })
704 }
705}
706
707fn paused_clock_at_unix_ms(unix_ms: i64) -> Arc<PausedClock> {
708 let nanos = (unix_ms as i128).saturating_mul(1_000_000);
709 let origin =
710 OffsetDateTime::from_unix_timestamp_nanos(nanos).unwrap_or(OffsetDateTime::UNIX_EPOCH);
711 PausedClock::new(origin)
712}
713
714pub(crate) fn vm_string(value: impl Into<String>) -> crate::VmValue {
715 crate::VmValue::String(Rc::from(value.into()))
716}
717
718impl Default for Harness {
719 fn default() -> Self {
720 Self::real()
721 }
722}
723
724#[derive(Debug, Clone)]
727pub struct HarnessStdio {
728 inner: Arc<HarnessInner>,
729}
730
731#[derive(Debug, Clone)]
733pub struct HarnessTerm {
734 inner: Arc<HarnessInner>,
735}
736
737#[derive(Debug, Clone)]
739pub struct HarnessClock {
740 inner: Arc<HarnessInner>,
741}
742
743impl HarnessClock {
744 pub fn clock(&self) -> &Arc<dyn Clock> {
745 self.inner.clock()
746 }
747}
748
749#[derive(Debug, Clone)]
752pub struct HarnessFs {
753 inner: Arc<HarnessInner>,
754}
755
756#[derive(Debug, Clone)]
758pub struct HarnessEnv {
759 inner: Arc<HarnessInner>,
760}
761
762#[derive(Debug, Clone)]
764pub struct HarnessRandom {
765 inner: Arc<HarnessInner>,
766}
767
768#[derive(Debug, Clone)]
770pub struct HarnessNet {
771 inner: Arc<HarnessInner>,
772}
773
774#[derive(Debug, Clone)]
776pub struct HarnessProcess {
777 inner: Arc<HarnessInner>,
778}
779
780#[derive(Debug, Clone)]
782pub struct HarnessCrypto {
783 inner: Arc<HarnessInner>,
784}
785
786#[derive(Debug, Clone)]
792pub struct HarnessSystem {
793 inner: Arc<HarnessInner>,
794}
795
796#[derive(Debug, Clone)]
798pub struct HarnessLlm {
799 inner: Arc<HarnessInner>,
800}
801
802#[derive(Debug, Clone)]
808pub struct HarnessTenant {
809 inner: Arc<HarnessInner>,
810}
811
812#[derive(Debug, Clone)]
822pub struct HarnessObs {
823 inner: Arc<HarnessInner>,
824}
825
826macro_rules! sub_handle_inner {
827 ($($ty:ty),* $(,)?) => {
828 $(
829 impl $ty {
830 #[allow(dead_code)]
831 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
832 &self.inner
833 }
834 }
835 )*
836 };
837}
838sub_handle_inner!(
839 HarnessStdio,
840 HarnessTerm,
841 HarnessFs,
842 HarnessEnv,
843 HarnessRandom,
844 HarnessNet,
845 HarnessProcess,
846 HarnessCrypto,
847 HarnessSystem,
848 HarnessLlm,
849 HarnessTenant,
850 HarnessObs,
851);
852
853impl HarnessClock {
854 #[allow(dead_code)]
855 pub(crate) fn inner(&self) -> &Arc<HarnessInner> {
856 &self.inner
857 }
858}
859
860#[derive(Clone)]
865pub struct VmHarness {
866 inner: Arc<HarnessInner>,
867 kind: HarnessKind,
868}
869
870impl VmHarness {
871 pub fn kind(&self) -> HarnessKind {
872 self.kind
873 }
874
875 pub fn type_name(&self) -> &'static str {
876 self.kind.type_name()
877 }
878
879 pub fn inner(&self) -> &Arc<HarnessInner> {
880 &self.inner
881 }
882
883 pub fn sub_handle(&self, field: &str) -> Option<VmHarness> {
887 if self.kind != HarnessKind::Root {
888 return None;
889 }
890 let kind = HarnessKind::from_field_name(field)?;
891 Some(VmHarness {
892 inner: Arc::clone(&self.inner),
893 kind,
894 })
895 }
896}
897
898impl fmt::Debug for VmHarness {
899 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
900 f.debug_struct("VmHarness")
901 .field("kind", &self.kind)
902 .finish_non_exhaustive()
903 }
904}
905
906#[derive(Debug)]
912pub struct MockAwareClock<C: Clock + 'static> {
913 inner: C,
914}
915
916impl<C: Clock + 'static> MockAwareClock<C> {
917 pub fn new(inner: C) -> Self {
918 Self { inner }
919 }
920}
921
922#[async_trait]
923impl<C: Clock + 'static> Clock for MockAwareClock<C> {
924 fn now_utc(&self) -> OffsetDateTime {
925 if let Some(mock) = crate::clock_mock::active_mock_clock() {
926 return mock.now_utc();
927 }
928 self.inner.now_utc()
929 }
930
931 fn monotonic_ms(&self) -> i64 {
932 if let Some(mock) = crate::clock_mock::active_mock_clock() {
933 return mock.monotonic_ms();
934 }
935 self.inner.monotonic_ms()
936 }
937
938 async fn sleep(&self, duration: Duration) {
939 if duration.is_zero() {
940 return;
941 }
942 if let Some(mock) = crate::clock_mock::active_mock_clock() {
943 mock.advance_std_sync(duration);
949 return;
950 }
951 self.inner.sleep(duration).await;
952 }
953
954 async fn sleep_until_utc(&self, deadline: OffsetDateTime) {
955 if let Some(mock) = crate::clock_mock::active_mock_clock() {
956 let now = mock.now_utc();
957 if deadline > now {
958 if let Ok(delta) = Duration::try_from(deadline - now) {
959 mock.advance_std_sync(delta);
960 }
961 }
962 return;
963 }
964 self.inner.sleep_until_utc(deadline).await;
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971
972 #[test]
973 fn real_constructs_without_panic() {
974 let _harness = Harness::real();
975 }
976
977 #[test]
978 fn sub_handles_share_inner_state() {
979 let harness = Harness::real();
980 let stdio_inner = Arc::as_ptr(harness.stdio().inner());
981 let clock_inner = Arc::as_ptr(harness.clock().inner());
982 assert_eq!(stdio_inner, clock_inner, "sub-handles share Arc<Inner>");
983 }
984
985 #[test]
986 fn kinds_round_trip_through_field_names() {
987 for kind in HarnessKind::SUB_HANDLES {
988 let field = kind.field_name().unwrap();
989 assert_eq!(HarnessKind::from_field_name(field), Some(*kind));
990 }
991 assert!(HarnessKind::from_field_name("nope").is_none());
992 assert!(HarnessKind::Root.field_name().is_none());
993 }
994
995 #[test]
996 fn vm_harness_property_access_returns_sub_handle() {
997 let root = match Harness::real().into_vm_value() {
998 crate::value::VmValue::Harness(h) => h,
999 other => panic!("expected Harness variant, got {}", other.type_name()),
1000 };
1001 let stdio = root.sub_handle("stdio").expect("stdio sub-handle");
1002 assert_eq!(stdio.kind(), HarnessKind::Stdio);
1003 assert!(stdio.sub_handle("clock").is_none(), "nested access denied");
1004 assert!(root.sub_handle("not_a_field").is_none());
1005 }
1006
1007 #[test]
1008 fn test_constructor_clock_advances_under_paused_clock_advance() {
1009 let (harness, paused) = Harness::test();
1010 let clock = harness.clock();
1011 let start_wall = clock.clock().now_utc();
1012 assert_eq!(start_wall, OffsetDateTime::UNIX_EPOCH);
1013 assert_eq!(clock.clock().monotonic_ms(), 0);
1014
1015 paused.advance(Duration::from_millis(1_500));
1016 assert_eq!(clock.clock().monotonic_ms(), 1_500);
1017 let after_wall = clock.clock().now_utc();
1018 assert_eq!(after_wall - start_wall, time::Duration::milliseconds(1_500));
1019 }
1020
1021 #[test]
1022 fn with_paused_clock_pins_origin() {
1023 let origin = OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
1024 let (harness, paused) = Harness::with_paused_clock(origin);
1025 assert_eq!(harness.clock().clock().now_utc(), origin);
1026 paused.advance(Duration::from_mins(1));
1027 assert_eq!(
1028 harness.clock().clock().now_utc() - origin,
1029 time::Duration::seconds(60)
1030 );
1031 }
1032
1033 #[test]
1034 fn null_harness_records_deny_events_for_every_sub_handle() {
1035 let harness = Harness::null();
1036 for source in [
1037 r#"fn main(harness: Harness) { harness.stdio.println("blocked") }"#,
1038 r"fn main(harness: Harness) { harness.term.width() }",
1039 r"fn main(harness: Harness) { harness.clock.now_ms() }",
1040 r#"fn main(harness: Harness) { harness.fs.read_text("/x") }"#,
1041 r#"fn main(harness: Harness) { harness.env.get("KEY") }"#,
1042 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1043 r#"fn main(harness: Harness) { harness.net.get("https://example.test") }"#,
1044 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1045 r#"fn main(harness: Harness) { harness.crypto.sha256("") }"#,
1046 r"fn main(harness: Harness) { harness.system.cpu() }",
1047 r"fn main(harness: Harness) { harness.llm.catalog() }",
1048 r"fn main(harness: Harness) { harness.tenant.id() }",
1049 r#"fn main(harness: Harness) { harness.obs.log("blocked", "info", {}) }"#,
1050 ] {
1051 let error = run_harness_source(source, harness.clone()).expect_err("call denied");
1052 assert!(
1053 error.contains("NullHarness denied"),
1054 "unexpected deny error: {error}"
1055 );
1056 }
1057
1058 let events = harness.deny_events();
1059 let observed: Vec<_> = events
1060 .iter()
1061 .map(|event| (event.sub_handle, event.method.as_str()))
1062 .collect();
1063 assert_eq!(
1064 observed,
1065 vec![
1066 (HarnessKind::Stdio, "println"),
1067 (HarnessKind::Term, "width"),
1068 (HarnessKind::Clock, "now_ms"),
1069 (HarnessKind::Fs, "read_text"),
1070 (HarnessKind::Env, "get"),
1071 (HarnessKind::Random, "gen_u64"),
1072 (HarnessKind::Net, "get"),
1073 (HarnessKind::Process, "spawn_captured"),
1074 (HarnessKind::Crypto, "sha256"),
1075 (HarnessKind::System, "cpu"),
1076 (HarnessKind::Llm, "catalog"),
1077 (HarnessKind::Tenant, "id"),
1078 (HarnessKind::Obs, "log"),
1079 ]
1080 );
1081 assert_eq!(events[0].args, vec!["blocked"]);
1082 assert_eq!(events[3].args, vec!["/x"]);
1083 }
1084
1085 #[test]
1086 fn mock_harness_replays_canned_responses_and_records_calls() {
1087 let harness = Harness::mock()
1088 .clock_at_unix_ms(1_700_000_000_000)
1089 .env("KEY", "value")
1090 .fs_read("/x", b"data".to_vec())
1091 .random_u64(42)
1092 .net_get("https://example.test", "body")
1093 .build();
1094
1095 let output = run_harness_source(
1096 r#"
1097fn main(harness: Harness) {
1098 harness.stdio.print("partial ")
1099 harness.stdio.println("line")
1100 __io_println(harness.term.width())
1101 __io_println(harness.term.height())
1102 __io_println(harness.clock.now_ms())
1103 harness.clock.sleep_ms(250)
1104 __io_println(harness.clock.now_ms())
1105 __io_println(harness.clock.monotonic_ms())
1106 __io_println(harness.env.get("KEY"))
1107 __io_println(harness.fs.read_text("/x"))
1108 __io_println(harness.fs.exists("/missing"))
1109 __io_println(harness.random.gen_u64())
1110 __io_println(harness.net.get("https://example.test"))
1111 __io_println(harness.crypto.sha256(""))
1112 __io_println(len(harness.llm.catalog()) > 0)
1113}
1114"#,
1115 harness.clone(),
1116 )
1117 .expect("mock harness run succeeds");
1118
1119 assert_eq!(harness.captured_stdio(), "partial line\n");
1120 assert_eq!(
1121 output,
1122 "80\n24\n1700000000000\n1700000000250\n250\nvalue\ndata\nfalse\n42\nbody\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ntrue\n"
1123 );
1124 let observed: Vec<_> = harness
1125 .calls()
1126 .into_iter()
1127 .map(|call| (call.sub_handle, call.method))
1128 .collect();
1129 assert_eq!(
1130 observed,
1131 vec![
1132 (HarnessKind::Stdio, "print".to_string()),
1133 (HarnessKind::Stdio, "println".to_string()),
1134 (HarnessKind::Term, "width".to_string()),
1135 (HarnessKind::Term, "height".to_string()),
1136 (HarnessKind::Clock, "now_ms".to_string()),
1137 (HarnessKind::Clock, "sleep_ms".to_string()),
1138 (HarnessKind::Clock, "now_ms".to_string()),
1139 (HarnessKind::Clock, "monotonic_ms".to_string()),
1140 (HarnessKind::Env, "get".to_string()),
1141 (HarnessKind::Fs, "read_text".to_string()),
1142 (HarnessKind::Fs, "exists".to_string()),
1143 (HarnessKind::Random, "gen_u64".to_string()),
1144 (HarnessKind::Net, "get".to_string()),
1145 (HarnessKind::Crypto, "sha256".to_string()),
1146 (HarnessKind::Llm, "catalog".to_string()),
1147 ]
1148 );
1149 }
1150
1151 #[test]
1152 fn mock_harness_replays_random_values_fifo() {
1153 let harness = Harness::mock()
1154 .random_u64(7)
1155 .random_u64(11)
1156 .random_u64(u64::MAX)
1157 .build();
1158
1159 let output = run_harness_source(
1160 r"
1161fn main(harness: Harness) {
1162 __io_println(harness.random.gen_u64())
1163 __io_println(harness.random.gen_u64())
1164 __io_println(harness.random.gen_u64())
1165}
1166",
1167 harness,
1168 )
1169 .expect("mock random succeeds");
1170
1171 assert_eq!(output, "7\n11\n9223372036854775807\n");
1172 }
1173
1174 #[test]
1175 fn mock_harness_reports_missing_canned_responses() {
1176 let cases = [
1177 (
1178 r#"fn main(harness: Harness) { harness.fs.read_text("/missing") }"#,
1179 "MockHarness has no fs_read response for /missing",
1180 ),
1181 (
1182 r"fn main(harness: Harness) { harness.random.gen_u64() }",
1183 "MockHarness has no random_u64 response",
1184 ),
1185 (
1186 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1187 "MockHarness has no net_get response for https://missing.test",
1188 ),
1189 (
1190 r#"fn main(harness: Harness) { harness.process.spawn_captured({cmd: "printf", args: ["x"]}) }"#,
1191 "MockHarness has no process spawn response",
1192 ),
1193 ];
1194
1195 for (source, expected) in cases {
1196 let error = run_harness_source(source, Harness::mock().build())
1197 .expect_err("missing mock response fails");
1198 assert!(
1199 error.contains(expected),
1200 "expected `{expected}` in `{error}`"
1201 );
1202 }
1203 }
1204
1205 #[test]
1206 fn mock_harness_records_failed_calls() {
1207 let harness = Harness::mock().build();
1208 let error = run_harness_source(
1209 r#"fn main(harness: Harness) { harness.net.get("https://missing.test") }"#,
1210 harness.clone(),
1211 )
1212 .expect_err("missing mock response fails");
1213
1214 assert!(error.contains("MockHarness has no net_get response"));
1215 assert_eq!(
1216 harness.calls(),
1217 vec![HarnessCall {
1218 sub_handle: HarnessKind::Net,
1219 method: "get".to_string(),
1220 args: vec!["https://missing.test".to_string()],
1221 }]
1222 );
1223 }
1224
1225 #[test]
1226 fn mock_harness_captures_stderr_separately_from_stdout() {
1227 let harness = Harness::mock().build();
1228 run_harness_source(
1229 r#"
1230fn main(harness: Harness) {
1231 harness.stdio.println("stdout line")
1232 harness.stdio.eprint("err ")
1233 harness.stdio.eprintln("trail")
1234}
1235"#,
1236 harness.clone(),
1237 )
1238 .expect("stderr capture run succeeds");
1239 assert_eq!(harness.captured_stdio(), "stdout line\n");
1240 assert_eq!(harness.captured_stderr(), "err trail\n");
1241 }
1242
1243 #[test]
1244 fn mock_harness_replays_stdin_lines_for_read_and_prompt() {
1245 let harness = Harness::mock()
1246 .stdin_line("first")
1247 .stdin_line("second")
1248 .build();
1249 let output = run_harness_source(
1250 r#"
1251fn main(harness: Harness) {
1252 harness.stdio.println(harness.stdio.read_line())
1253 harness.stdio.println(harness.stdio.prompt("answer: "))
1254 let eof = harness.stdio.read_line({trim: false})
1255 harness.stdio.println(eof.status)
1256}
1257"#,
1258 harness.clone(),
1259 )
1260 .expect("stdin replay succeeds");
1261 assert_eq!(output, "");
1263 assert_eq!(harness.captured_stdio(), "first\nanswer: second\neof\n");
1264 }
1265
1266 #[test]
1267 fn mock_harness_replays_password_input_without_stdout_echo() {
1268 let harness = Harness::mock().stdin_line("secret").build();
1269 let output = run_harness_source(
1270 r#"
1271fn main(harness: Harness) {
1272 __io_println(harness.term.read_password("password: "))
1273}
1274"#,
1275 harness.clone(),
1276 )
1277 .expect("stdin replay succeeds");
1278
1279 assert_eq!(output, "secret\n");
1280 assert_eq!(harness.captured_stdio(), "");
1281 assert_eq!(harness.captured_stderr(), "password: ");
1282 assert_eq!(
1283 harness.calls(),
1284 vec![HarnessCall {
1285 sub_handle: HarnessKind::Term,
1286 method: "read_password".to_string(),
1287 args: vec!["password: ".to_string()],
1288 }]
1289 );
1290 }
1291
1292 #[test]
1293 fn mock_harness_rejects_wrong_argument_types() {
1294 let error = run_harness_source(
1295 r"fn main(harness: Harness) { harness.fs.read_text(1) }",
1296 Harness::mock().build(),
1297 )
1298 .expect_err("wrong argument type fails");
1299
1300 let runtime_rejection =
1310 error.contains("HarnessFs.read_text expects string argument 1, got int");
1311 let static_rejection = error.contains("argument 1 `path`: expected string, found int");
1312 assert!(
1313 runtime_rejection || static_rejection,
1314 "expected a string/int type rejection for read_text, got: {error}"
1315 );
1316 }
1317
1318 #[test]
1319 fn real_harness_fs_write_outside_workspace_roots_surfaces_cap_201() {
1320 use crate::orchestration::{
1321 clear_execution_policy_stacks, push_execution_policy, CapabilityPolicy, SandboxProfile,
1322 };
1323 clear_execution_policy_stacks();
1324 let temp = tempfile::tempdir().unwrap();
1325 let policy = CapabilityPolicy {
1326 sandbox_profile: SandboxProfile::Worktree,
1327 workspace_roots: vec![temp.path().to_string_lossy().into_owned()],
1328 ..CapabilityPolicy::default()
1329 };
1330 push_execution_policy(policy);
1331 let outside = std::env::temp_dir().join("harn_e4_4_cap_201_outside.txt");
1332 let source = format!(
1333 r#"fn main(harness: Harness) {{ harness.fs.write_text("{}", "x") }}"#,
1334 outside.to_string_lossy().replace('\\', "/"),
1335 );
1336 let error = run_harness_source(&source, Harness::real())
1337 .expect_err("write outside workspace_roots must reject");
1338 clear_execution_policy_stacks();
1339 assert!(
1340 error.contains("HARN-CAP-201"),
1341 "expected HARN-CAP-201 prefix, got: {error}"
1342 );
1343 assert!(
1344 error.contains("sandbox violation"),
1345 "deny should keep the underlying sandbox-rejection message, got: {error}"
1346 );
1347 }
1348
1349 fn run_harness_source(source: &str, harness: Harness) -> Result<String, String> {
1350 let rt = tokio::runtime::Builder::new_current_thread()
1351 .enable_all()
1352 .build()
1353 .unwrap();
1354 rt.block_on(async move {
1355 let local = tokio::task::LocalSet::new();
1356 local
1357 .run_until(async move {
1358 let chunk = crate::compile_source(source)?;
1359 let mut vm = crate::Vm::new();
1360 crate::stdlib::register_vm_stdlib(&mut vm);
1361 vm.set_harness(harness);
1362 vm.execute(&chunk)
1363 .await
1364 .map_err(|error| error.to_string())?;
1365 Ok(vm.output().to_string())
1366 })
1367 .await
1368 })
1369 }
1370}